build: go 1.24

We were running behind and there were quite some deprecations to update.
This was mostly in the upstream copy/pasta package but seems quite
minimal.
This commit is contained in:
2025-03-16 12:04:32 +01:00
parent a2b678caf6
commit 1723025fbf
822 changed files with 25433 additions and 197407 deletions

View File

@ -49,16 +49,16 @@ func ShiftNBytesLeft(dst, x []byte, n int) {
dst = append(dst, make([]byte, n/8)...)
}
// XorBytesMut assumes equal input length, replaces X with X XOR Y
// XorBytesMut replaces X with X XOR Y. len(X) must be >= len(Y).
func XorBytesMut(X, Y []byte) {
for i := 0; i < len(X); i++ {
for i := 0; i < len(Y); i++ {
X[i] ^= Y[i]
}
}
// XorBytes assumes equal input length, puts X XOR Y into Z
// XorBytes puts X XOR Y into Z. len(Z) and len(X) must be >= len(Y).
func XorBytes(Z, X, Y []byte) {
for i := 0; i < len(X); i++ {
for i := 0; i < len(Y); i++ {
Z[i] = X[i] ^ Y[i]
}
}

View File

@ -109,8 +109,10 @@ func (o *ocb) Seal(dst, nonce, plaintext, adata []byte) []byte {
if len(nonce) > o.nonceSize {
panic("crypto/ocb: Incorrect nonce length given to OCB")
}
ret, out := byteutil.SliceForAppend(dst, len(plaintext)+o.tagSize)
o.crypt(enc, out, nonce, adata, plaintext)
sep := len(plaintext)
ret, out := byteutil.SliceForAppend(dst, sep+o.tagSize)
tag := o.crypt(enc, out[:sep], nonce, adata, plaintext)
copy(out[sep:], tag)
return ret
}
@ -122,12 +124,10 @@ func (o *ocb) Open(dst, nonce, ciphertext, adata []byte) ([]byte, error) {
return nil, ocbError("Ciphertext shorter than tag length")
}
sep := len(ciphertext) - o.tagSize
ret, out := byteutil.SliceForAppend(dst, len(ciphertext))
ret, out := byteutil.SliceForAppend(dst, sep)
ciphertextData := ciphertext[:sep]
tag := ciphertext[sep:]
o.crypt(dec, out, nonce, adata, ciphertextData)
if subtle.ConstantTimeCompare(ret[sep:], tag) == 1 {
ret = ret[:sep]
tag := o.crypt(dec, out, nonce, adata, ciphertextData)
if subtle.ConstantTimeCompare(tag, ciphertext[sep:]) == 1 {
return ret, nil
}
for i := range out {
@ -137,7 +137,8 @@ func (o *ocb) Open(dst, nonce, ciphertext, adata []byte) ([]byte, error) {
}
// On instruction enc (resp. dec), crypt is the encrypt (resp. decrypt)
// function. It returns the resulting plain/ciphertext with the tag appended.
// function. It writes the resulting plain/ciphertext into Y and returns
// the tag.
func (o *ocb) crypt(instruction int, Y, nonce, adata, X []byte) []byte {
//
// Consider X as a sequence of 128-bit blocks
@ -194,13 +195,14 @@ func (o *ocb) crypt(instruction int, Y, nonce, adata, X []byte) []byte {
byteutil.XorBytesMut(offset, o.mask.L[bits.TrailingZeros(uint(i+1))])
blockX := X[i*blockSize : (i+1)*blockSize]
blockY := Y[i*blockSize : (i+1)*blockSize]
byteutil.XorBytes(blockY, blockX, offset)
switch instruction {
case enc:
byteutil.XorBytesMut(checksum, blockX)
byteutil.XorBytes(blockY, blockX, offset)
o.block.Encrypt(blockY, blockY)
byteutil.XorBytesMut(blockY, offset)
byteutil.XorBytesMut(checksum, blockX)
case dec:
byteutil.XorBytes(blockY, blockX, offset)
o.block.Decrypt(blockY, blockY)
byteutil.XorBytesMut(blockY, offset)
byteutil.XorBytesMut(checksum, blockY)
@ -216,31 +218,24 @@ func (o *ocb) crypt(instruction int, Y, nonce, adata, X []byte) []byte {
o.block.Encrypt(pad, offset)
chunkX := X[blockSize*m:]
chunkY := Y[blockSize*m : len(X)]
byteutil.XorBytes(chunkY, chunkX, pad[:len(chunkX)])
// P_* || bit(1) || zeroes(127) - len(P_*)
switch instruction {
case enc:
paddedY := append(chunkX, byte(128))
paddedY = append(paddedY, make([]byte, blockSize-len(chunkX)-1)...)
byteutil.XorBytesMut(checksum, paddedY)
byteutil.XorBytesMut(checksum, chunkX)
checksum[len(chunkX)] ^= 128
byteutil.XorBytes(chunkY, chunkX, pad[:len(chunkX)])
// P_* || bit(1) || zeroes(127) - len(P_*)
case dec:
paddedX := append(chunkY, byte(128))
paddedX = append(paddedX, make([]byte, blockSize-len(chunkY)-1)...)
byteutil.XorBytesMut(checksum, paddedX)
byteutil.XorBytes(chunkY, chunkX, pad[:len(chunkX)])
// P_* || bit(1) || zeroes(127) - len(P_*)
byteutil.XorBytesMut(checksum, chunkY)
checksum[len(chunkY)] ^= 128
}
byteutil.XorBytes(tag, checksum, offset)
byteutil.XorBytesMut(tag, o.mask.lDol)
o.block.Encrypt(tag, tag)
byteutil.XorBytesMut(tag, o.hash(adata))
copy(Y[blockSize*m+len(chunkY):], tag[:o.tagSize])
} else {
byteutil.XorBytes(tag, checksum, offset)
byteutil.XorBytesMut(tag, o.mask.lDol)
o.block.Encrypt(tag, tag)
byteutil.XorBytesMut(tag, o.hash(adata))
copy(Y[blockSize*m:], tag[:o.tagSize])
}
return Y
byteutil.XorBytes(tag, checksum, offset)
byteutil.XorBytesMut(tag, o.mask.lDol)
o.block.Encrypt(tag, tag)
byteutil.XorBytesMut(tag, o.hash(adata))
return tag[:o.tagSize]
}
// This hash function is used to compute the tag. Per design, on empty input it

View File

@ -7,6 +7,7 @@ package armor
import (
"encoding/base64"
"io"
"sort"
)
var armorHeaderSep = []byte(": ")
@ -159,8 +160,15 @@ func encode(out io.Writer, blockType string, headers map[string]string, checksum
return
}
for k, v := range headers {
err = writeSlices(out, []byte(k), armorHeaderSep, []byte(v), newline)
keys := make([]string, len(headers))
i := 0
for k := range headers {
keys[i] = k
i++
}
sort.Strings(keys)
for _, k := range keys {
err = writeSlices(out, []byte(k), armorHeaderSep, []byte(headers[k]), newline)
if err != nil {
return
}

View File

@ -6,6 +6,7 @@
package errors // import "github.com/ProtonMail/go-crypto/openpgp/errors"
import (
"fmt"
"strconv"
)
@ -178,3 +179,22 @@ type ErrMalformedMessage string
func (dke ErrMalformedMessage) Error() string {
return "openpgp: malformed message " + string(dke)
}
// ErrEncryptionKeySelection is returned if encryption key selection fails (v2 API).
type ErrEncryptionKeySelection struct {
PrimaryKeyId string
PrimaryKeyErr error
EncSelectionKeyId *string
EncSelectionErr error
}
func (eks ErrEncryptionKeySelection) Error() string {
prefix := fmt.Sprintf("openpgp: key selection for primary key %s:", eks.PrimaryKeyId)
if eks.PrimaryKeyErr != nil {
return fmt.Sprintf("%s invalid primary key: %s", prefix, eks.PrimaryKeyErr)
}
if eks.EncSelectionKeyId != nil {
return fmt.Sprintf("%s invalid encryption key %s: %s", prefix, *eks.EncSelectionKeyId, eks.EncSelectionErr)
}
return fmt.Sprintf("%s no encryption key: %s", prefix, eks.EncSelectionErr)
}

View File

@ -3,7 +3,6 @@
package packet
import (
"bytes"
"crypto/cipher"
"encoding/binary"
"io"
@ -15,12 +14,11 @@ import (
type aeadCrypter struct {
aead cipher.AEAD
chunkSize int
initialNonce []byte
nonce []byte
associatedData []byte // Chunk-independent associated data
chunkIndex []byte // Chunk counter
packetTag packetType // SEIP packet (v2) or AEAD Encrypted Data packet
bytesProcessed int // Amount of plaintext bytes encrypted/decrypted
buffer bytes.Buffer // Buffered bytes across chunks
}
// computeNonce takes the incremental index and computes an eXclusive OR with
@ -28,12 +26,12 @@ type aeadCrypter struct {
// 5.16.1 and 5.16.2). It returns the resulting nonce.
func (wo *aeadCrypter) computeNextNonce() (nonce []byte) {
if wo.packetTag == packetTypeSymmetricallyEncryptedIntegrityProtected {
return append(wo.initialNonce, wo.chunkIndex...)
return wo.nonce
}
nonce = make([]byte, len(wo.initialNonce))
copy(nonce, wo.initialNonce)
offset := len(wo.initialNonce) - 8
nonce = make([]byte, len(wo.nonce))
copy(nonce, wo.nonce)
offset := len(wo.nonce) - 8
for i := 0; i < 8; i++ {
nonce[i+offset] ^= wo.chunkIndex[i]
}
@ -62,8 +60,9 @@ func (wo *aeadCrypter) incrementIndex() error {
type aeadDecrypter struct {
aeadCrypter // Embedded ciphertext opener
reader io.Reader // 'reader' is a partialLengthReader
chunkBytes []byte
peekedBytes []byte // Used to detect last chunk
eof bool
buffer []byte // Buffered decrypted bytes
}
// Read decrypts bytes and reads them into dst. It decrypts when necessary and
@ -71,59 +70,44 @@ type aeadDecrypter struct {
// and an error.
func (ar *aeadDecrypter) Read(dst []byte) (n int, err error) {
// Return buffered plaintext bytes from previous calls
if ar.buffer.Len() > 0 {
return ar.buffer.Read(dst)
}
// Return EOF if we've previously validated the final tag
if ar.eof {
return 0, io.EOF
if len(ar.buffer) > 0 {
n = copy(dst, ar.buffer)
ar.buffer = ar.buffer[n:]
return
}
// Read a chunk
tagLen := ar.aead.Overhead()
cipherChunkBuf := new(bytes.Buffer)
_, errRead := io.CopyN(cipherChunkBuf, ar.reader, int64(ar.chunkSize+tagLen))
cipherChunk := cipherChunkBuf.Bytes()
if errRead != nil && errRead != io.EOF {
copy(ar.chunkBytes, ar.peekedBytes) // Copy bytes peeked in previous chunk or in initialization
bytesRead, errRead := io.ReadFull(ar.reader, ar.chunkBytes[tagLen:])
if errRead != nil && errRead != io.EOF && errRead != io.ErrUnexpectedEOF {
return 0, errRead
}
if len(cipherChunk) > 0 {
decrypted, errChunk := ar.openChunk(cipherChunk)
if bytesRead > 0 {
ar.peekedBytes = ar.chunkBytes[bytesRead:bytesRead+tagLen]
decrypted, errChunk := ar.openChunk(ar.chunkBytes[:bytesRead])
if errChunk != nil {
return 0, errChunk
}
// Return decrypted bytes, buffering if necessary
if len(dst) < len(decrypted) {
n = copy(dst, decrypted[:len(dst)])
ar.buffer.Write(decrypted[len(dst):])
} else {
n = copy(dst, decrypted)
}
n = copy(dst, decrypted)
ar.buffer = decrypted[n:]
return
}
// Check final authentication tag
if errRead == io.EOF {
errChunk := ar.validateFinalTag(ar.peekedBytes)
if errChunk != nil {
return n, errChunk
}
ar.eof = true // Mark EOF for when we've returned all buffered data
}
return
return 0, io.EOF
}
// Close is noOp. The final authentication tag of the stream was already
// checked in the last Read call. In the future, this function could be used to
// wipe the reader and peeked, decrypted bytes, if necessary.
// Close checks the final authentication tag of the stream.
// In the future, this function could also be used to wipe the reader
// and peeked & decrypted bytes, if necessary.
func (ar *aeadDecrypter) Close() (err error) {
if !ar.eof {
errChunk := ar.validateFinalTag(ar.peekedBytes)
if errChunk != nil {
return errChunk
}
errChunk := ar.validateFinalTag(ar.peekedBytes)
if errChunk != nil {
return errChunk
}
return nil
}
@ -132,20 +116,13 @@ func (ar *aeadDecrypter) Close() (err error) {
// the underlying plaintext and an error. It accesses peeked bytes from next
// chunk, to identify the last chunk and decrypt/validate accordingly.
func (ar *aeadDecrypter) openChunk(data []byte) ([]byte, error) {
tagLen := ar.aead.Overhead()
// Restore carried bytes from last call
chunkExtra := append(ar.peekedBytes, data...)
// 'chunk' contains encrypted bytes, followed by an authentication tag.
chunk := chunkExtra[:len(chunkExtra)-tagLen]
ar.peekedBytes = chunkExtra[len(chunkExtra)-tagLen:]
adata := ar.associatedData
if ar.aeadCrypter.packetTag == packetTypeAEADEncrypted {
adata = append(ar.associatedData, ar.chunkIndex...)
}
nonce := ar.computeNextNonce()
plainChunk, err := ar.aead.Open(nil, nonce, chunk, adata)
plainChunk, err := ar.aead.Open(data[:0:len(data)], nonce, data, adata)
if err != nil {
return nil, errors.ErrAEADTagVerification
}
@ -183,27 +160,29 @@ func (ar *aeadDecrypter) validateFinalTag(tag []byte) error {
type aeadEncrypter struct {
aeadCrypter // Embedded plaintext sealer
writer io.WriteCloser // 'writer' is a partialLengthWriter
chunkBytes []byte
offset int
}
// Write encrypts and writes bytes. It encrypts when necessary and buffers extra
// plaintext bytes for next call. When the stream is finished, Close() MUST be
// called to append the final tag.
func (aw *aeadEncrypter) Write(plaintextBytes []byte) (n int, err error) {
// Append plaintextBytes to existing buffered bytes
n, err = aw.buffer.Write(plaintextBytes)
if err != nil {
return n, err
}
// Encrypt and write chunks
for aw.buffer.Len() >= aw.chunkSize {
plainChunk := aw.buffer.Next(aw.chunkSize)
encryptedChunk, err := aw.sealChunk(plainChunk)
if err != nil {
return n, err
}
_, err = aw.writer.Write(encryptedChunk)
if err != nil {
return n, err
for n != len(plaintextBytes) {
copied := copy(aw.chunkBytes[aw.offset:aw.chunkSize], plaintextBytes[n:])
n += copied
aw.offset += copied
if aw.offset == aw.chunkSize {
encryptedChunk, err := aw.sealChunk(aw.chunkBytes[:aw.offset])
if err != nil {
return n, err
}
_, err = aw.writer.Write(encryptedChunk)
if err != nil {
return n, err
}
aw.offset = 0
}
}
return
@ -215,9 +194,8 @@ func (aw *aeadEncrypter) Write(plaintextBytes []byte) (n int, err error) {
func (aw *aeadEncrypter) Close() (err error) {
// Encrypt and write a chunk if there's buffered data left, or if we haven't
// written any chunks yet.
if aw.buffer.Len() > 0 || aw.bytesProcessed == 0 {
plainChunk := aw.buffer.Bytes()
lastEncryptedChunk, err := aw.sealChunk(plainChunk)
if aw.offset > 0 || aw.bytesProcessed == 0 {
lastEncryptedChunk, err := aw.sealChunk(aw.chunkBytes[:aw.offset])
if err != nil {
return err
}
@ -263,7 +241,7 @@ func (aw *aeadEncrypter) sealChunk(data []byte) ([]byte, error) {
}
nonce := aw.computeNextNonce()
encrypted := aw.aead.Seal(nil, nonce, data, adata)
encrypted := aw.aead.Seal(data[:0], nonce, data, adata)
aw.bytesProcessed += len(data)
if err := aw.aeadCrypter.incrementIndex(); err != nil {
return nil, err

View File

@ -65,24 +65,28 @@ func (ae *AEADEncrypted) decrypt(key []byte) (io.ReadCloser, error) {
blockCipher := ae.cipher.new(key)
aead := ae.mode.new(blockCipher)
// Carry the first tagLen bytes
chunkSize := decodeAEADChunkSize(ae.chunkSizeByte)
tagLen := ae.mode.TagLength()
peekedBytes := make([]byte, tagLen)
chunkBytes := make([]byte, chunkSize+tagLen*2)
peekedBytes := chunkBytes[chunkSize+tagLen:]
n, err := io.ReadFull(ae.Contents, peekedBytes)
if n < tagLen || (err != nil && err != io.EOF) {
return nil, errors.AEADError("Not enough data to decrypt:" + err.Error())
}
chunkSize := decodeAEADChunkSize(ae.chunkSizeByte)
return &aeadDecrypter{
aeadCrypter: aeadCrypter{
aead: aead,
chunkSize: chunkSize,
initialNonce: ae.initialNonce,
nonce: ae.initialNonce,
associatedData: ae.associatedData(),
chunkIndex: make([]byte, 8),
packetTag: packetTypeAEADEncrypted,
},
reader: ae.Contents,
peekedBytes: peekedBytes}, nil
chunkBytes: chunkBytes,
peekedBytes: peekedBytes,
}, nil
}
// associatedData for chunks: tag, version, cipher, mode, chunk size byte

View File

@ -173,6 +173,11 @@ type Config struct {
// weaknesses in the hash algo, potentially hindering e.g. some chosen-prefix attacks.
// The default behavior, when the config or flag is nil, is to enable the feature.
NonDeterministicSignaturesViaNotation *bool
// InsecureAllowAllKeyFlagsWhenMissing determines how a key without valid key flags is handled.
// When set to true, a key without flags is treated as if all flags are enabled.
// This behavior is consistent with GPG.
InsecureAllowAllKeyFlagsWhenMissing bool
}
func (c *Config) Random() io.Reader {
@ -403,6 +408,13 @@ func (c *Config) RandomizeSignaturesViaNotation() bool {
return *c.NonDeterministicSignaturesViaNotation
}
func (c *Config) AllowAllKeyFlagsWhenMissing() bool {
if c == nil {
return false
}
return c.InsecureAllowAllKeyFlagsWhenMissing
}
// BoolPointer is a helper function to set a boolean pointer in the Config.
// e.g., config.CheckPacketSequence = BoolPointer(true)
func BoolPointer(value bool) *bool {

View File

@ -1048,12 +1048,17 @@ func (pk *PublicKey) VerifyDirectKeySignature(sig *Signature) (err error) {
// KeyIdString returns the public key's fingerprint in capital hex
// (e.g. "6C7EE1B8621CC013").
func (pk *PublicKey) KeyIdString() string {
return fmt.Sprintf("%X", pk.Fingerprint[12:20])
return fmt.Sprintf("%016X", pk.KeyId)
}
// KeyIdShortString returns the short form of public key's fingerprint
// in capital hex, as shown by gpg --list-keys (e.g. "621CC013").
// This function will return the full key id for v5 and v6 keys
// since the short key id is undefined for them.
func (pk *PublicKey) KeyIdShortString() string {
if pk.Version >= 5 {
return pk.KeyIdString()
}
return fmt.Sprintf("%X", pk.Fingerprint[16:20])
}

View File

@ -1288,7 +1288,9 @@ func (sig *Signature) buildSubpackets(issuer PublicKey) (subpackets []outputSubp
if sig.IssuerKeyId != nil && sig.Version == 4 {
keyId := make([]byte, 8)
binary.BigEndian.PutUint64(keyId, *sig.IssuerKeyId)
subpackets = append(subpackets, outputSubpacket{true, issuerSubpacket, true, keyId})
// Note: making this critical breaks RPM <=4.16.
// See: https://github.com/ProtonMail/go-crypto/issues/263
subpackets = append(subpackets, outputSubpacket{true, issuerSubpacket, false, keyId})
}
// Notation Data
for _, notation := range sig.Notations {

View File

@ -70,8 +70,10 @@ func (se *SymmetricallyEncrypted) decryptAead(inputKey []byte) (io.ReadCloser, e
aead, nonce := getSymmetricallyEncryptedAeadInstance(se.Cipher, se.Mode, inputKey, se.Salt[:], se.associatedData())
// Carry the first tagLen bytes
chunkSize := decodeAEADChunkSize(se.ChunkSizeByte)
tagLen := se.Mode.TagLength()
peekedBytes := make([]byte, tagLen)
chunkBytes := make([]byte, chunkSize+tagLen*2)
peekedBytes := chunkBytes[chunkSize+tagLen:]
n, err := io.ReadFull(se.Contents, peekedBytes)
if n < tagLen || (err != nil && err != io.EOF) {
return nil, errors.StructuralError("not enough data to decrypt:" + err.Error())
@ -81,12 +83,13 @@ func (se *SymmetricallyEncrypted) decryptAead(inputKey []byte) (io.ReadCloser, e
aeadCrypter: aeadCrypter{
aead: aead,
chunkSize: decodeAEADChunkSize(se.ChunkSizeByte),
initialNonce: nonce,
nonce: nonce,
associatedData: se.associatedData(),
chunkIndex: make([]byte, 8),
chunkIndex: nonce[len(nonce)-8:],
packetTag: packetTypeSymmetricallyEncryptedIntegrityProtected,
},
reader: se.Contents,
chunkBytes: chunkBytes,
peekedBytes: peekedBytes,
}, nil
}
@ -130,16 +133,20 @@ func serializeSymmetricallyEncryptedAead(ciphertext io.WriteCloser, cipherSuite
aead, nonce := getSymmetricallyEncryptedAeadInstance(cipherSuite.Cipher, cipherSuite.Mode, inputKey, salt, prefix)
chunkSize := decodeAEADChunkSize(chunkSizeByte)
tagLen := aead.Overhead()
chunkBytes := make([]byte, chunkSize+tagLen)
return &aeadEncrypter{
aeadCrypter: aeadCrypter{
aead: aead,
chunkSize: decodeAEADChunkSize(chunkSizeByte),
chunkSize: chunkSize,
associatedData: prefix,
chunkIndex: make([]byte, 8),
initialNonce: nonce,
nonce: nonce,
chunkIndex: nonce[len(nonce)-8:],
packetTag: packetTypeSymmetricallyEncryptedIntegrityProtected,
},
writer: ciphertext,
writer: ciphertext,
chunkBytes: chunkBytes,
}, nil
}
@ -149,10 +156,10 @@ func getSymmetricallyEncryptedAeadInstance(c CipherFunction, mode AEADMode, inpu
encryptionKey := make([]byte, c.KeySize())
_, _ = readFull(hkdfReader, encryptionKey)
// Last 64 bits of nonce are the counter
nonce = make([]byte, mode.IvLength()-8)
nonce = make([]byte, mode.IvLength())
_, _ = readFull(hkdfReader, nonce)
// Last 64 bits of nonce are the counter
_, _ = readFull(hkdfReader, nonce[:len(nonce)-8])
blockCipher := c.new(encryptionKey)
aead = mode.new(blockCipher)

View File

@ -1,5 +1,6 @@
run:
tests: false
issues-exit-code: 0
issues:
include:
@ -36,5 +37,4 @@ linters:
- govet
- ineffassign
- staticcheck
- typecheck
- unused

View File

@ -0,0 +1,28 @@
run:
tests: false
issues:
include:
- EXC0001
- EXC0005
- EXC0011
- EXC0012
- EXC0013
max-issues-per-linter: 0
max-same-issues: 0
linters:
enable:
- bodyclose
- gofumpt
- goimports
- gosec
- nilerr
- revive
- rowserrcheck
- sqlclosecheck
- tparallel
- unconvert
- unparam
- whitespace

View File

@ -0,0 +1,6 @@
includes:
- from_url:
url: charmbracelet/meta/main/goreleaser-lib.yaml
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json

21
vendor/github.com/charmbracelet/colorprofile/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2024 Charmbracelet, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

103
vendor/github.com/charmbracelet/colorprofile/README.md generated vendored Normal file
View File

@ -0,0 +1,103 @@
# Colorprofile
<p>
<a href="https://github.com/charmbracelet/colorprofile/releases"><img src="https://img.shields.io/github/release/charmbracelet/colorprofile.svg" alt="Latest Release"></a>
<a href="https://pkg.go.dev/github.com/charmbracelet/colorprofile?tab=doc"><img src="https://godoc.org/github.com/charmbracelet/colorprofile?status.svg" alt="GoDoc"></a>
<a href="https://github.com/charmbracelet/colorprofile/actions"><img src="https://github.com/charmbracelet/colorprofile/actions/workflows/build.yml/badge.svg" alt="Build Status"></a>
</p>
A simple, powerful—and at times magical—package for detecting terminal color
profiles and performing color (and CSI) degradation.
## Detecting the terminals color profile
Detecting the terminals color profile is easy.
```go
import "github.com/charmbracelet/colorprofile"
// Detect the color profile. If youre planning on writing to stderr you'd want
// to use os.Stderr instead.
p := colorprofile.Detect(os.Stdout, os.Environ())
// Comment on the profile.
fmt.Printf("You know, your colors are quite %s.", func() string {
switch p {
case colorprofile.TrueColor:
return "fancy"
case colorprofile.ANSI256:
return "1990s fancy"
case colorprofile.ANSI:
return "normcore"
case colorprofile.Ascii:
return "ancient"
case colorprofile.NoTTY:
return "naughty!"
}
return "...IDK" // this should never happen
}())
```
## Downsampling colors
When necessary, colors can be downsampled to a given profile, or manually
downsampled to a specific profile.
```go
p := colorprofile.Detect(os.Stdout, os.Environ())
c := color.RGBA{0x6b, 0x50, 0xff, 0xff} // #6b50ff
// Downsample to the detected profile, when necessary.
convertedColor := p.Convert(c)
// Or manually convert to a given profile.
ansi256Color := colorprofile.ANSI256.Convert(c)
ansiColor := colorprofile.ANSI.Convert(c)
noColor := colorprofile.Ascii.Convert(c)
noANSI := colorprofile.NoTTY.Convert(c)
```
## Automatic downsampling with a Writer
You can also magically downsample colors in ANSI output, when necessary. If
output is not a TTY ANSI will be dropped entirely.
```go
myFancyANSI := "\x1b[38;2;107;80;255mCute \x1b[1;3mpuppy!!\x1b[m"
// Automatically downsample for the terminal at stdout.
w := colorprofile.NewWriter(os.Stdout, os.Environ())
fmt.Fprintf(w, myFancyANSI)
// Downsample to 4-bit ANSI.
w.Profile = colorprofile.ANSI
fmt.Fprintf(w, myFancyANSI)
// Ascii-fy, no colors.
w.Profile = colorprofile.Ascii
fmt.Fprintf(w, myFancyANSI)
// Strip ANSI altogether.
w.Profile = colorprofile.NoTTY
fmt.Fprintf(w, myFancyANSI) // not as fancy
```
## Feedback
Wed love to hear your thoughts on this project. Feel free to drop us a note!
- [Twitter](https://twitter.com/charmcli)
- [The Fediverse](https://mastodon.social/@charmcli)
- [Discord](https://charm.sh/chat)
## License
[MIT](https://github.com/charmbracelet/bubbletea/raw/master/LICENSE)
---
Part of [Charm](https://charm.sh).
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة

287
vendor/github.com/charmbracelet/colorprofile/env.go generated vendored Normal file
View File

@ -0,0 +1,287 @@
package colorprofile
import (
"bytes"
"io"
"os/exec"
"runtime"
"strconv"
"strings"
"github.com/charmbracelet/x/term"
"github.com/xo/terminfo"
)
// Detect returns the color profile based on the terminal output, and
// environment variables. This respects NO_COLOR, CLICOLOR, and CLICOLOR_FORCE
// environment variables.
//
// The rules as follows:
// - TERM=dumb is always treated as NoTTY unless CLICOLOR_FORCE=1 is set.
// - If COLORTERM=truecolor, and the profile is not NoTTY, it gest upgraded to TrueColor.
// - Using any 256 color terminal (e.g. TERM=xterm-256color) will set the profile to ANSI256.
// - Using any color terminal (e.g. TERM=xterm-color) will set the profile to ANSI.
// - Using CLICOLOR=1 without TERM defined should be treated as ANSI if the
// output is a terminal.
// - NO_COLOR takes precedence over CLICOLOR/CLICOLOR_FORCE, and will disable
// colors but not text decoration, i.e. bold, italic, faint, etc.
//
// 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"
envp := colorProfile(isatty, environ)
if envp == TrueColor || envNoColor(environ) {
// We already know we have TrueColor, or NO_COLOR is set.
return envp
}
if isatty && !isDumb {
tip := Terminfo(term)
tmuxp := tmux(environ)
// Color profile is the maximum of env, terminfo, and tmux.
return max(envp, max(tip, tmuxp))
}
return envp
}
// Env returns the color profile based on the terminal environment variables.
// This respects NO_COLOR, CLICOLOR, and CLICOLOR_FORCE environment variables.
//
// The rules as follows:
// - TERM=dumb is always treated as NoTTY unless CLICOLOR_FORCE=1 is set.
// - If COLORTERM=truecolor, and the profile is not NoTTY, it gest upgraded to TrueColor.
// - Using any 256 color terminal (e.g. TERM=xterm-256color) will set the profile to ANSI256.
// - Using any color terminal (e.g. TERM=xterm-color) will set the profile to ANSI.
// - Using CLICOLOR=1 without TERM defined should be treated as ANSI if the
// output is a terminal.
// - NO_COLOR takes precedence over CLICOLOR/CLICOLOR_FORCE, and will disable
// colors but not text decoration, i.e. bold, italic, faint, etc.
//
// See https://no-color.org/ and https://bixense.com/clicolors/ for more information.
func Env(env []string) (p Profile) {
return colorProfile(true, newEnviron(env))
}
func colorProfile(isatty bool, env environ) (p Profile) {
isDumb := env.get("TERM") == "dumb"
envp := envColorProfile(env)
if !isatty || isDumb {
// Check if the output is a terminal.
// Treat dumb terminals as NoTTY
p = NoTTY
} else {
p = envp
}
if envNoColor(env) && isatty {
if p > Ascii {
p = Ascii
}
return
}
if cliColorForced(env) {
if p < ANSI {
p = ANSI
}
if envp > p {
p = envp
}
return
}
if cliColor(env) {
if isatty && !isDumb && p < ANSI {
p = ANSI
}
}
return p
}
// envNoColor returns true if the environment variables explicitly disable color output
// by setting NO_COLOR (https://no-color.org/).
func envNoColor(env environ) bool {
noColor, _ := strconv.ParseBool(env.get("NO_COLOR"))
return noColor
}
func cliColor(env environ) bool {
cliColor, _ := strconv.ParseBool(env.get("CLICOLOR"))
return cliColor
}
func cliColorForced(env environ) bool {
cliColorForce, _ := strconv.ParseBool(env.get("CLICOLOR_FORCE"))
return cliColorForce
}
func colorTerm(env environ) bool {
colorTerm := strings.ToLower(env.get("COLORTERM"))
return colorTerm == "truecolor" || colorTerm == "24bit" ||
colorTerm == "yes" || colorTerm == "true"
}
// 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" {
p = NoTTY
if runtime.GOOS == "windows" {
// Use Windows API to detect color profile. Windows Terminal and
// cmd.exe don't define $TERM.
if wcp, ok := windowsColorProfile(env); ok {
p = wcp
}
}
} else {
p = ANSI
}
parts := strings.Split(term, "-")
switch parts[0] {
case "alacritty",
"contour",
"foot",
"ghostty",
"kitty",
"rio",
"st",
"wezterm":
return TrueColor
case "xterm":
if len(parts) > 1 {
switch parts[1] {
case "ghostty", "kitty":
// These terminals can be defined as xterm-TERMNAME
return TrueColor
}
}
case "tmux", "screen":
if p < ANSI256 {
p = ANSI256
}
}
if isCloudShell, _ := strconv.ParseBool(env.get("GOOGLE_CLOUD_SHELL")); isCloudShell {
return TrueColor
}
// GNU Screen doesn't support TrueColor
// Tmux doesn't support $COLORTERM
if colorTerm(env) && !strings.HasPrefix(term, "screen") && !strings.HasPrefix(term, "tmux") {
return TrueColor
}
if strings.HasSuffix(term, "256color") && p < ANSI256 {
p = ANSI256
}
return
}
// Terminfo returns the color profile based on the terminal's terminfo
// database. This relies on the Tc and RGB capabilities to determine if the
// terminal supports TrueColor.
// If term is empty or "dumb", it returns NoTTY.
func Terminfo(term string) (p Profile) {
if len(term) == 0 || term == "dumb" {
return NoTTY
}
p = ANSI
ti, err := terminfo.Load(term)
if err != nil {
return
}
extbools := ti.ExtBoolCapsShort()
if _, ok := extbools["Tc"]; ok {
return TrueColor
}
if _, ok := extbools["RGB"]; ok {
return TrueColor
}
return
}
// Tmux returns the color profile based on `tmux info` output. Tmux supports
// overriding the terminal's color capabilities, so this function will return
// the color profile based on the tmux configuration.
func Tmux(env []string) Profile {
return tmux(newEnviron(env))
}
// tmux returns the color profile based on the tmux environment variables.
func tmux(env environ) (p Profile) {
if tmux, ok := env.lookup("TMUX"); !ok || len(tmux) == 0 {
// Not in tmux
return NoTTY
}
// Check if tmux has either Tc or RGB capabilities. Otherwise, return
// ANSI256.
p = ANSI256
cmd := exec.Command("tmux", "info")
out, err := cmd.Output()
if err != nil {
return
}
for _, line := range bytes.Split(out, []byte("\n")) {
if (bytes.Contains(line, []byte("Tc")) || bytes.Contains(line, []byte("RGB"))) &&
bytes.Contains(line, []byte("true")) {
return TrueColor
}
}
return
}
// environ is a map of environment variables.
type environ map[string]string
// newEnviron returns a new environment map from a slice of environment
// variables.
func newEnviron(environ []string) environ {
m := make(map[string]string, len(environ))
for _, e := range environ {
parts := strings.SplitN(e, "=", 2)
var value string
if len(parts) == 2 {
value = parts[1]
}
m[parts[0]] = value
}
return m
}
// lookup returns the value of an environment variable and a boolean indicating
// if it exists.
func (e environ) lookup(key string) (string, bool) {
v, ok := e[key]
return v, ok
}
// get returns the value of an environment variable and empty string if it
// doesn't exist.
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

@ -0,0 +1,8 @@
//go:build !windows
// +build !windows
package colorprofile
func windowsColorProfile(map[string]string) (Profile, bool) {
return 0, false
}

View File

@ -0,0 +1,45 @@
//go:build windows
// +build windows
package colorprofile
import (
"strconv"
"golang.org/x/sys/windows"
)
func windowsColorProfile(env map[string]string) (Profile, bool) {
if env["ConEmuANSI"] == "ON" {
return TrueColor, true
}
if len(env["WT_SESSION"]) > 0 {
// Windows Terminal supports TrueColor
return TrueColor, true
}
major, _, build := windows.RtlGetNtVersionNumbers()
if build < 10586 || major < 10 {
// No ANSI support before WindowsNT 10 build 10586
if len(env["ANSICON"]) > 0 {
ansiconVer := env["ANSICON_VER"]
cv, err := strconv.Atoi(ansiconVer)
if err != nil || cv < 181 {
// No 8 bit color support before ANSICON 1.81
return ANSI, true
}
return ANSI256, true
}
return NoTTY, true
}
if build < 14931 {
// No true color support before build 14931
return ANSI256, true
}
return TrueColor, true
}

399
vendor/github.com/charmbracelet/colorprofile/profile.go generated vendored Normal file
View File

@ -0,0 +1,399 @@
package colorprofile
import (
"image/color"
"math"
"github.com/charmbracelet/x/ansi"
"github.com/lucasb-eyer/go-colorful"
)
// Profile is a color profile: NoTTY, Ascii, ANSI, ANSI256, or TrueColor.
type Profile byte
const (
// NoTTY, not a terminal profile.
NoTTY Profile = iota
// Ascii, uncolored profile.
Ascii //nolint:revive
// ANSI, 4-bit color profile.
ANSI
// ANSI256, 8-bit color profile.
ANSI256
// TrueColor, 24-bit color profile.
TrueColor
)
// String returns the string representation of a Profile.
func (p Profile) String() string {
switch p {
case TrueColor:
return "TrueColor"
case ANSI256:
return "ANSI256"
case ANSI:
return "ANSI"
case Ascii:
return "Ascii"
case NoTTY:
return "NoTTY"
}
return "Unknown"
}
// Convert transforms a given Color to a Color supported within the Profile.
func (p Profile) Convert(c color.Color) color.Color {
if p <= Ascii {
return nil
}
switch c := c.(type) {
case ansi.BasicColor:
return c
case ansi.ExtendedColor:
if p == ANSI {
return ansi256ToANSIColor(c)
}
return c
case ansi.TrueColor, color.Color:
h, ok := colorful.MakeColor(c)
if !ok {
return nil
}
if p != TrueColor {
ac := hexToANSI256Color(h)
if p == ANSI {
return ansi256ToANSIColor(ac)
}
return ac
}
return c
}
return c
}
func hexToANSI256Color(c colorful.Color) ansi.ExtendedColor {
v2ci := func(v float64) int {
if v < 48 {
return 0
}
if v < 115 {
return 1
}
return int((v - 35) / 40)
}
// Calculate the nearest 0-based color index at 16..231
r := v2ci(c.R * 255.0) // 0..5 each
g := v2ci(c.G * 255.0)
b := v2ci(c.B * 255.0)
ci := 36*r + 6*g + b /* 0..215 */
// Calculate the represented colors back from the index
i2cv := [6]int{0, 0x5f, 0x87, 0xaf, 0xd7, 0xff}
cr := i2cv[r] // r/g/b, 0..255 each
cg := i2cv[g]
cb := i2cv[b]
// Calculate the nearest 0-based gray index at 232..255
var grayIdx int
average := (cr + cg + cb) / 3
if average > 238 {
grayIdx = 23
} else {
grayIdx = (average - 3) / 10 // 0..23
}
gv := 8 + 10*grayIdx // same value for r/g/b, 0..255
// Return the one which is nearer to the original input rgb value
c2 := colorful.Color{R: float64(cr) / 255.0, G: float64(cg) / 255.0, B: float64(cb) / 255.0}
g2 := colorful.Color{R: float64(gv) / 255.0, G: float64(gv) / 255.0, B: float64(gv) / 255.0}
colorDist := c.DistanceHSLuv(c2)
grayDist := c.DistanceHSLuv(g2)
if colorDist <= grayDist {
return ansi.ExtendedColor(16 + ci) //nolint:gosec
}
return ansi.ExtendedColor(232 + grayIdx) //nolint:gosec
}
func ansi256ToANSIColor(c ansi.ExtendedColor) ansi.BasicColor {
var r int
md := math.MaxFloat64
h, _ := colorful.Hex(ansiHex[c])
for i := 0; i <= 15; i++ {
hb, _ := colorful.Hex(ansiHex[i])
d := h.DistanceHSLuv(hb)
if d < md {
md = d
r = i
}
}
return ansi.BasicColor(r) //nolint:gosec
}
// RGB values of ANSI colors (0-255).
var ansiHex = []string{
"#000000",
"#800000",
"#008000",
"#808000",
"#000080",
"#800080",
"#008080",
"#c0c0c0",
"#808080",
"#ff0000",
"#00ff00",
"#ffff00",
"#0000ff",
"#ff00ff",
"#00ffff",
"#ffffff",
"#000000",
"#00005f",
"#000087",
"#0000af",
"#0000d7",
"#0000ff",
"#005f00",
"#005f5f",
"#005f87",
"#005faf",
"#005fd7",
"#005fff",
"#008700",
"#00875f",
"#008787",
"#0087af",
"#0087d7",
"#0087ff",
"#00af00",
"#00af5f",
"#00af87",
"#00afaf",
"#00afd7",
"#00afff",
"#00d700",
"#00d75f",
"#00d787",
"#00d7af",
"#00d7d7",
"#00d7ff",
"#00ff00",
"#00ff5f",
"#00ff87",
"#00ffaf",
"#00ffd7",
"#00ffff",
"#5f0000",
"#5f005f",
"#5f0087",
"#5f00af",
"#5f00d7",
"#5f00ff",
"#5f5f00",
"#5f5f5f",
"#5f5f87",
"#5f5faf",
"#5f5fd7",
"#5f5fff",
"#5f8700",
"#5f875f",
"#5f8787",
"#5f87af",
"#5f87d7",
"#5f87ff",
"#5faf00",
"#5faf5f",
"#5faf87",
"#5fafaf",
"#5fafd7",
"#5fafff",
"#5fd700",
"#5fd75f",
"#5fd787",
"#5fd7af",
"#5fd7d7",
"#5fd7ff",
"#5fff00",
"#5fff5f",
"#5fff87",
"#5fffaf",
"#5fffd7",
"#5fffff",
"#870000",
"#87005f",
"#870087",
"#8700af",
"#8700d7",
"#8700ff",
"#875f00",
"#875f5f",
"#875f87",
"#875faf",
"#875fd7",
"#875fff",
"#878700",
"#87875f",
"#878787",
"#8787af",
"#8787d7",
"#8787ff",
"#87af00",
"#87af5f",
"#87af87",
"#87afaf",
"#87afd7",
"#87afff",
"#87d700",
"#87d75f",
"#87d787",
"#87d7af",
"#87d7d7",
"#87d7ff",
"#87ff00",
"#87ff5f",
"#87ff87",
"#87ffaf",
"#87ffd7",
"#87ffff",
"#af0000",
"#af005f",
"#af0087",
"#af00af",
"#af00d7",
"#af00ff",
"#af5f00",
"#af5f5f",
"#af5f87",
"#af5faf",
"#af5fd7",
"#af5fff",
"#af8700",
"#af875f",
"#af8787",
"#af87af",
"#af87d7",
"#af87ff",
"#afaf00",
"#afaf5f",
"#afaf87",
"#afafaf",
"#afafd7",
"#afafff",
"#afd700",
"#afd75f",
"#afd787",
"#afd7af",
"#afd7d7",
"#afd7ff",
"#afff00",
"#afff5f",
"#afff87",
"#afffaf",
"#afffd7",
"#afffff",
"#d70000",
"#d7005f",
"#d70087",
"#d700af",
"#d700d7",
"#d700ff",
"#d75f00",
"#d75f5f",
"#d75f87",
"#d75faf",
"#d75fd7",
"#d75fff",
"#d78700",
"#d7875f",
"#d78787",
"#d787af",
"#d787d7",
"#d787ff",
"#d7af00",
"#d7af5f",
"#d7af87",
"#d7afaf",
"#d7afd7",
"#d7afff",
"#d7d700",
"#d7d75f",
"#d7d787",
"#d7d7af",
"#d7d7d7",
"#d7d7ff",
"#d7ff00",
"#d7ff5f",
"#d7ff87",
"#d7ffaf",
"#d7ffd7",
"#d7ffff",
"#ff0000",
"#ff005f",
"#ff0087",
"#ff00af",
"#ff00d7",
"#ff00ff",
"#ff5f00",
"#ff5f5f",
"#ff5f87",
"#ff5faf",
"#ff5fd7",
"#ff5fff",
"#ff8700",
"#ff875f",
"#ff8787",
"#ff87af",
"#ff87d7",
"#ff87ff",
"#ffaf00",
"#ffaf5f",
"#ffaf87",
"#ffafaf",
"#ffafd7",
"#ffafff",
"#ffd700",
"#ffd75f",
"#ffd787",
"#ffd7af",
"#ffd7d7",
"#ffd7ff",
"#ffff00",
"#ffff5f",
"#ffff87",
"#ffffaf",
"#ffffd7",
"#ffffff",
"#080808",
"#121212",
"#1c1c1c",
"#262626",
"#303030",
"#3a3a3a",
"#444444",
"#4e4e4e",
"#585858",
"#626262",
"#6c6c6c",
"#767676",
"#808080",
"#8a8a8a",
"#949494",
"#9e9e9e",
"#a8a8a8",
"#b2b2b2",
"#bcbcbc",
"#c6c6c6",
"#d0d0d0",
"#dadada",
"#e4e4e4",
"#eeeeee",
}

166
vendor/github.com/charmbracelet/colorprofile/writer.go generated vendored Normal file
View File

@ -0,0 +1,166 @@
package colorprofile
import (
"bytes"
"image/color"
"io"
"strconv"
"github.com/charmbracelet/x/ansi"
)
// NewWriter creates a new color profile writer that downgrades color sequences
// based on the detected color profile.
//
// If environ is nil, it will use os.Environ() to get the environment variables.
//
// It queries the given writer to determine if it supports ANSI escape codes.
// If it does, along with the given environment variables, it will determine
// the appropriate color profile to use for color formatting.
//
// This respects the NO_COLOR, CLICOLOR, and CLICOLOR_FORCE environment variables.
func NewWriter(w io.Writer, environ []string) *Writer {
return &Writer{
Forward: w,
Profile: Detect(w, environ),
}
}
// Writer represents a color profile writer that writes ANSI sequences to the
// underlying writer.
type Writer struct {
Forward io.Writer
Profile Profile
}
// Write writes the given text to the underlying writer.
func (w *Writer) Write(p []byte) (int, error) {
switch w.Profile {
case TrueColor:
return w.Forward.Write(p)
case NoTTY:
return io.WriteString(w.Forward, ansi.Strip(string(p)))
default:
return w.downsample(p)
}
}
// downsample downgrades the given text to the appropriate color profile.
func (w *Writer) downsample(p []byte) (int, error) {
var buf bytes.Buffer
var state byte
parser := ansi.GetParser()
defer ansi.PutParser(parser)
for len(p) > 0 {
parser.Reset()
seq, _, read, newState := ansi.DecodeSequence(p, state, parser)
switch {
case ansi.HasCsiPrefix(seq) && parser.Command() == 'm':
handleSgr(w, parser, &buf)
default:
// If we're not a style SGR sequence, just write the bytes.
if n, err := buf.Write(seq); err != nil {
return n, err
}
}
p = p[read:]
state = newState
}
return w.Forward.Write(buf.Bytes())
}
// WriteString writes the given text to the underlying writer.
func (w *Writer) WriteString(s string) (n int, err error) {
return w.Write([]byte(s))
}
func handleSgr(w *Writer, p *ansi.Parser, buf *bytes.Buffer) {
var style ansi.Style
params := p.Params()
for i := 0; i < len(params); i++ {
param := params[i]
switch param := param.Param(0); param {
case 0:
// SGR default parameter is 0. We use an empty string to reduce the
// number of bytes written to the buffer.
style = append(style, "")
case 30, 31, 32, 33, 34, 35, 36, 37: // 8-bit foreground color
if w.Profile < ANSI {
continue
}
style = style.ForegroundColor(
w.Profile.Convert(ansi.BasicColor(param - 30))) //nolint:gosec
case 38: // 16 or 24-bit foreground color
var c color.Color
if n := ansi.ReadStyleColor(params[i:], &c); n > 0 {
i += n - 1
}
if w.Profile < ANSI {
continue
}
style = style.ForegroundColor(w.Profile.Convert(c))
case 39: // default foreground color
if w.Profile < ANSI {
continue
}
style = style.DefaultForegroundColor()
case 40, 41, 42, 43, 44, 45, 46, 47: // 8-bit background color
if w.Profile < ANSI {
continue
}
style = style.BackgroundColor(
w.Profile.Convert(ansi.BasicColor(param - 40))) //nolint:gosec
case 48: // 16 or 24-bit background color
var c color.Color
if n := ansi.ReadStyleColor(params[i:], &c); n > 0 {
i += n - 1
}
if w.Profile < ANSI {
continue
}
style = style.BackgroundColor(w.Profile.Convert(c))
case 49: // default background color
if w.Profile < ANSI {
continue
}
style = style.DefaultBackgroundColor()
case 58: // 16 or 24-bit underline color
var c color.Color
if n := ansi.ReadStyleColor(params[i:], &c); n > 0 {
i += n - 1
}
if w.Profile < ANSI {
continue
}
style = style.UnderlineColor(w.Profile.Convert(c))
case 59: // default underline color
if w.Profile < ANSI {
continue
}
style = style.DefaultUnderlineColor()
case 90, 91, 92, 93, 94, 95, 96, 97: // 8-bit bright foreground color
if w.Profile < ANSI {
continue
}
style = style.ForegroundColor(
w.Profile.Convert(ansi.BasicColor(param - 90 + 8))) //nolint:gosec
case 100, 101, 102, 103, 104, 105, 106, 107: // 8-bit bright background color
if w.Profile < ANSI {
continue
}
style = style.BackgroundColor(
w.Profile.Convert(ansi.BasicColor(param - 100 + 8))) //nolint:gosec
default:
// If this is not a color attribute, just append it to the style.
style = append(style, strconv.Itoa(param))
}
}
_, _ = buf.WriteString(style.String())
}

View File

@ -1 +1,2 @@
ssh_example_ed25519*
dist/

View File

@ -15,10 +15,22 @@ issues:
linters:
enable:
- bodyclose
- exhaustive
- goconst
- godot
- godox
- gofumpt
- goimports
- gomoddirectives
- goprintffuncname
- gosec
- misspell
- nakedret
- nestif
- nilerr
- noctx
- nolintlint
- prealloc
- revive
- rowserrcheck
- sqlclosecheck
@ -26,3 +38,4 @@ linters:
- unconvert
- unparam
- whitespace
- wrapcheck

View File

@ -1,5 +1,5 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
version: 2
includes:
- from_url:
url: charmbracelet/meta/main/goreleaser-lib.yaml
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json

View File

@ -10,7 +10,7 @@
Style definitions for nice terminal layouts. Built with TUIs in mind.
![Lip Gloss example](https://github.com/user-attachments/assets/99c5c015-551b-4897-8cd1-bcaafa0aad5a)
![Lip Gloss example](https://github.com/user-attachments/assets/7950b1c1-e0e3-427e-8e7d-6f7f6ad17ca7)
Lip Gloss takes an expressive, declarative approach to terminal rendering.
Users familiar with CSS will feel at home with Lip Gloss.
@ -425,17 +425,28 @@ rows := [][]string{
Use the table package to style and render the table.
```go
var (
purple = lipgloss.Color("99")
gray = lipgloss.Color("245")
lightGray = lipgloss.Color("241")
headerStyle = lipgloss.NewStyle().Foreground(purple).Bold(true).Align(lipgloss.Center)
cellStyle = lipgloss.NewStyle().Padding(0, 1).Width(14)
oddRowStyle = cellStyle.Foreground(gray)
evenRowStyle = cellStyle.Foreground(lightGray)
)
t := table.New().
Border(lipgloss.NormalBorder()).
BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("99"))).
BorderStyle(lipgloss.NewStyle().Foreground(purple)).
StyleFunc(func(row, col int) lipgloss.Style {
switch {
case row == 0:
return HeaderStyle
case row == table.HeaderRow:
return headerStyle
case row%2 == 0:
return EvenRowStyle
return evenRowStyle
default:
return OddRowStyle
return oddRowStyle
}
}).
Headers("LANGUAGE", "FORMAL", "INFORMAL").
@ -453,6 +464,45 @@ fmt.Println(t)
![Table Example](https://github.com/charmbracelet/lipgloss/assets/42545625/6e4b70c4-f494-45da-a467-bdd27df30d5d)
> [!WARNING]
> Table `Rows` need to be declared before `Offset` otherwise it does nothing.
### Table Borders
There are helpers to generate tables in markdown or ASCII style:
#### Markdown Table
```go
table.New().Border(lipgloss.MarkdownBorder()).BorderTop(false).BorderBottom(false)
```
```
| LANGUAGE | FORMAL | INFORMAL |
|----------|--------------|-----------|
| Chinese | Nǐn hǎo | Nǐ hǎo |
| French | Bonjour | Salut |
| Russian | Zdravstvuyte | Privet |
| Spanish | Hola | ¿Qué tal? |
```
#### ASCII Table
```go
table.New().Border(lipgloss.ASCIIBorder())
```
```
+----------+--------------+-----------+
| LANGUAGE | FORMAL | INFORMAL |
+----------+--------------+-----------+
| Chinese | Nǐn hǎo | Nǐ hǎo |
| French | Bonjour | Salut |
| Russian | Zdravstvuyte | Privet |
| Spanish | Hola | ¿Qué tal? |
+----------+--------------+-----------+
```
For more on tables see [the docs](https://pkg.go.dev/github.com/charmbracelet/lipgloss?tab=doc) and [examples](https://github.com/charmbracelet/lipgloss/tree/master/examples/table).
## Rendering Lists

19
vendor/github.com/charmbracelet/lipgloss/Taskfile.yaml generated vendored Normal file
View File

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

View File

@ -30,8 +30,8 @@ func alignTextHorizontal(str string, pos Position, width int, style *termenv.Sty
l = s + l
case Center:
// Note: remainder goes on the right.
left := shortAmount / 2 //nolint:gomnd
right := left + shortAmount%2 //nolint:gomnd
left := shortAmount / 2 //nolint:mnd
right := left + shortAmount%2 //nolint:mnd
leftSpaces := strings.Repeat(" ", left)
rightSpaces := strings.Repeat(" ", right)
@ -69,7 +69,7 @@ func alignTextVertical(str string, pos Position, height int, _ *termenv.Style) s
case Top:
return str + strings.Repeat("\n", height-strHeight)
case Center:
topPadding, bottomPadding := (height-strHeight)/2, (height-strHeight)/2 //nolint:gomnd
topPadding, bottomPadding := (height-strHeight)/2, (height-strHeight)/2 //nolint:mnd
if strHeight+topPadding+bottomPadding > height {
topPadding--
} else if strHeight+topPadding+bottomPadding < height {

View File

@ -100,14 +100,19 @@ var (
}
blockBorder = Border{
Top: "█",
Bottom: "█",
Left: "█",
Right: "█",
TopLeft: "█",
TopRight: "█",
BottomLeft: "█",
BottomRight: "█",
Top: "█",
Bottom: "█",
Left: "█",
Right: "█",
TopLeft: "█",
TopRight: "█",
BottomLeft: "█",
BottomRight: "█",
MiddleLeft: "█",
MiddleRight: "█",
Middle: "█",
MiddleTop: "█",
MiddleBottom: "█",
}
outerHalfBlockBorder = Border{
@ -179,6 +184,38 @@ var (
MiddleTop: " ",
MiddleBottom: " ",
}
markdownBorder = Border{
Top: "-",
Bottom: "-",
Left: "|",
Right: "|",
TopLeft: "|",
TopRight: "|",
BottomLeft: "|",
BottomRight: "|",
MiddleLeft: "|",
MiddleRight: "|",
Middle: "|",
MiddleTop: "|",
MiddleBottom: "|",
}
asciiBorder = Border{
Top: "-",
Bottom: "-",
Left: "|",
Right: "|",
TopLeft: "+",
TopRight: "+",
BottomLeft: "+",
BottomRight: "+",
MiddleLeft: "+",
MiddleRight: "+",
Middle: "+",
MiddleTop: "+",
MiddleBottom: "+",
}
)
// NormalBorder returns a standard-type border with a normal weight and 90
@ -226,13 +263,23 @@ func HiddenBorder() Border {
return hiddenBorder
}
// MarkdownBorder return a table border in markdown style.
//
// Make sure to disable top and bottom border for the best result. This will
// ensure that the output is valid markdown.
//
// table.New().Border(lipgloss.MarkdownBorder()).BorderTop(false).BorderBottom(false)
func MarkdownBorder() Border {
return markdownBorder
}
// ASCIIBorder returns a table border with ASCII characters.
func ASCIIBorder() Border {
return asciiBorder
}
func (s Style) applyBorder(str string) string {
var (
topSet = s.isSet(borderTopKey)
rightSet = s.isSet(borderRightKey)
bottomSet = s.isSet(borderBottomKey)
leftSet = s.isSet(borderLeftKey)
border = s.getBorderStyle()
hasTop = s.getAsBool(borderTopKey, false)
hasRight = s.getAsBool(borderRightKey, false)
@ -252,7 +299,7 @@ func (s Style) applyBorder(str string) string {
// If a border is set and no sides have been specifically turned on or off
// render borders on all sides.
if border != noBorder && !(topSet || rightSet || bottomSet || leftSet) {
if s.implicitBorders() {
hasTop = true
hasRight = true
hasBottom = true

View File

@ -35,7 +35,7 @@ func (NoColor) color(*Renderer) termenv.Color {
//
// Deprecated.
func (n NoColor) RGBA() (r, g, b, a uint32) {
return 0x0, 0x0, 0x0, 0xFFFF //nolint:gomnd
return 0x0, 0x0, 0x0, 0xFFFF //nolint:mnd
}
// Color specifies a color by hex or ANSI value. For example:

View File

@ -300,7 +300,7 @@ func (s Style) GetBorderTopWidth() int {
// runes of varying widths, the widest rune is returned. If no border exists on
// the top edge, 0 is returned.
func (s Style) GetBorderTopSize() int {
if !s.getAsBool(borderTopKey, false) {
if !s.getAsBool(borderTopKey, false) && !s.implicitBorders() {
return 0
}
return s.getBorderStyle().GetTopSize()
@ -310,7 +310,7 @@ func (s Style) GetBorderTopSize() int {
// runes of varying widths, the widest rune is returned. If no border exists on
// the left edge, 0 is returned.
func (s Style) GetBorderLeftSize() int {
if !s.getAsBool(borderLeftKey, false) {
if !s.getAsBool(borderLeftKey, false) && !s.implicitBorders() {
return 0
}
return s.getBorderStyle().GetLeftSize()
@ -320,7 +320,7 @@ func (s Style) GetBorderLeftSize() int {
// contain runes of varying widths, the widest rune is returned. If no border
// exists on the left edge, 0 is returned.
func (s Style) GetBorderBottomSize() int {
if !s.getAsBool(borderBottomKey, false) {
if !s.getAsBool(borderBottomKey, false) && !s.implicitBorders() {
return 0
}
return s.getBorderStyle().GetBottomSize()
@ -330,7 +330,7 @@ func (s Style) GetBorderBottomSize() int {
// contain runes of varying widths, the widest rune is returned. If no border
// exists on the right edge, 0 is returned.
func (s Style) GetBorderRightSize() int {
if !s.getAsBool(borderRightKey, false) {
if !s.getAsBool(borderRightKey, false) && !s.implicitBorders() {
return 0
}
return s.getBorderStyle().GetRightSize()
@ -519,6 +519,20 @@ func (s Style) getBorderStyle() Border {
return s.borderStyle
}
// Returns whether or not the style has implicit borders. This happens when
// a border style has been set but no border sides have been explicitly turned
// on or off.
func (s Style) implicitBorders() bool {
var (
borderStyle = s.getBorderStyle()
topSet = s.isSet(borderTopKey)
rightSet = s.isSet(borderRightKey)
bottomSet = s.isSet(borderBottomKey)
leftSet = s.isSet(borderLeftKey)
)
return borderStyle != noBorder && !(topSet || rightSet || bottomSet || leftSet)
}
func (s Style) getAsTransform(propKey) func(string) string {
if !s.isSet(transformKey) {
return nil

48
vendor/github.com/charmbracelet/lipgloss/ranges.go generated vendored Normal file
View File

@ -0,0 +1,48 @@
package lipgloss
import (
"strings"
"github.com/charmbracelet/x/ansi"
)
// StyleRanges allows to, given a string, style ranges of it differently.
// The function will take into account existing styles.
// Ranges should not overlap.
func StyleRanges(s string, ranges ...Range) string {
if len(ranges) == 0 {
return s
}
var buf strings.Builder
lastIdx := 0
stripped := ansi.Strip(s)
// Use Truncate and TruncateLeft to style match.MatchedIndexes without
// losing the original option style:
for _, rng := range ranges {
// Add the text before this match
if rng.Start > lastIdx {
buf.WriteString(ansi.Cut(s, lastIdx, rng.Start))
}
// Add the matched range with its highlight
buf.WriteString(rng.Style.Render(ansi.Cut(stripped, rng.Start, rng.End)))
lastIdx = rng.End
}
// Add any remaining text after the last match
buf.WriteString(ansi.TruncateLeft(s, lastIdx, ""))
return buf.String()
}
// NewRange returns a range that can be used with [StyleRanges].
func NewRange(start, end int, style Style) Range {
return Range{start, end, style}
}
// Range to be used with [StyleRanges].
type Range struct {
Start, End int
Style Style
}

View File

@ -710,19 +710,19 @@ func whichSidesInt(i ...int) (top, right, bottom, left int, ok bool) {
left = i[0]
right = i[0]
ok = true
case 2: //nolint:gomnd
case 2: //nolint:mnd
top = i[0]
bottom = i[0]
left = i[1]
right = i[1]
ok = true
case 3: //nolint:gomnd
case 3: //nolint:mnd
top = i[0]
left = i[1]
right = i[1]
bottom = i[2]
ok = true
case 4: //nolint:gomnd
case 4: //nolint:mnd
top = i[0]
right = i[1]
bottom = i[2]
@ -743,19 +743,19 @@ func whichSidesBool(i ...bool) (top, right, bottom, left bool, ok bool) {
left = i[0]
right = i[0]
ok = true
case 2: //nolint:gomnd
case 2: //nolint:mnd
top = i[0]
bottom = i[0]
left = i[1]
right = i[1]
ok = true
case 3: //nolint:gomnd
case 3: //nolint:mnd
top = i[0]
left = i[1]
right = i[1]
bottom = i[2]
ok = true
case 4: //nolint:gomnd
case 4: //nolint:mnd
top = i[0]
right = i[1]
bottom = i[2]
@ -776,19 +776,19 @@ func whichSidesColor(i ...TerminalColor) (top, right, bottom, left TerminalColor
left = i[0]
right = i[0]
ok = true
case 2: //nolint:gomnd
case 2: //nolint:mnd
top = i[0]
bottom = i[0]
left = i[1]
right = i[1]
ok = true
case 3: //nolint:gomnd
case 3: //nolint:mnd
top = i[0]
left = i[1]
right = i[1]
bottom = i[2]
ok = true
case 4: //nolint:gomnd
case 4: //nolint:mnd
top = i[0]
right = i[1]
bottom = i[2]

View File

@ -5,6 +5,7 @@ import (
"unicode"
"github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/cellbuf"
"github.com/muesli/termenv"
)
@ -364,7 +365,7 @@ func (s Style) Render(strs ...string) string {
// Word wrap
if !inline && width > 0 {
wrapAt := width - leftPadding - rightPadding
str = ansi.Wrap(str, wrapAt, "")
str = cellbuf.Wrap(str, wrapAt, "")
}
// Render core text
@ -431,7 +432,7 @@ func (s Style) Render(strs ...string) string {
{
numLines := strings.Count(str, "\n")
if !(numLines == 0 && width == 0) {
if numLines != 0 || width != 0 {
var st *termenv.Style
if colorWhitespace || styleWhitespace {
st = &teWhitespace

View File

@ -0,0 +1,418 @@
package table
import (
"math"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
)
// resize resizes the table to fit the specified width.
//
// Given a user defined table width, we must ensure the table is exactly that
// width. This must account for all borders, column, separators, and column
// data.
//
// In the case where the table is narrower than the specified table width,
// we simply expand the columns evenly to fit the width.
// For example, a table with 3 columns takes up 50 characters total, and the
// width specified is 80, we expand each column by 10 characters, adding 30
// to the total width.
//
// In the case where the table is wider than the specified table width, we
// _could_ simply shrink the columns evenly but this would result in data
// being truncated (perhaps unnecessarily). The naive approach could result
// in very poor cropping of the table data. So, instead of shrinking columns
// evenly, we calculate the median non-whitespace length of each column, and
// shrink the columns based on the largest median.
//
// For example,
//
// ┌──────┬───────────────┬──────────┐
// │ Name │ Age of Person │ Location │
// ├──────┼───────────────┼──────────┤
// │ Kini │ 40 │ New York │
// │ Eli │ 30 │ London │
// │ Iris │ 20 │ Paris │
// └──────┴───────────────┴──────────┘
//
// Median non-whitespace length vs column width of each column:
//
// Name: 4 / 5
// Age of Person: 2 / 15
// Location: 6 / 10
//
// The biggest difference is 15 - 2, so we can shrink the 2nd column by 13.
func (t *Table) resize() {
hasHeaders := len(t.headers) > 0
rows := dataToMatrix(t.data)
r := newResizer(t.width, t.height, t.headers, rows)
r.wrap = t.wrap
r.borderColumn = t.borderColumn
r.yPaddings = make([][]int, len(r.allRows))
var allRows [][]string
if hasHeaders {
allRows = append([][]string{t.headers}, rows...)
} else {
allRows = rows
}
r.rowHeights = r.defaultRowHeights()
for i, row := range allRows {
r.yPaddings[i] = make([]int, len(row))
for j := range row {
column := &r.columns[j]
// Making sure we're passing the right index to `styleFunc`. The header row should be `-1` and
// the others should start from `0`.
rowIndex := i
if hasHeaders {
rowIndex--
}
style := t.styleFunc(rowIndex, j)
topMargin, rightMargin, bottomMargin, leftMargin := style.GetMargin()
topPadding, rightPadding, bottomPadding, leftPadding := style.GetPadding()
totalHorizontalPadding := leftMargin + rightMargin + leftPadding + rightPadding
column.xPadding = max(column.xPadding, totalHorizontalPadding)
column.fixedWidth = max(column.fixedWidth, style.GetWidth())
r.rowHeights[i] = max(r.rowHeights[i], style.GetHeight())
totalVerticalPadding := topMargin + bottomMargin + topPadding + bottomPadding
r.yPaddings[i][j] = totalVerticalPadding
}
}
// A table width wasn't specified. In this case, detect according to
// content width.
if r.tableWidth <= 0 {
r.tableWidth = r.detectTableWidth()
}
t.widths, t.heights = r.optimizedWidths()
}
// resizerColumn is a column in the resizer.
type resizerColumn struct {
index int
min int
max int
median int
rows [][]string
xPadding int // horizontal padding
fixedWidth int
}
// resizer is a table resizer.
type resizer struct {
tableWidth int
tableHeight int
headers []string
allRows [][]string
rowHeights []int
columns []resizerColumn
wrap bool
borderColumn bool
yPaddings [][]int // vertical paddings
}
// newResizer creates a new resizer.
func newResizer(tableWidth, tableHeight int, headers []string, rows [][]string) *resizer {
r := &resizer{
tableWidth: tableWidth,
tableHeight: tableHeight,
headers: headers,
}
if len(headers) > 0 {
r.allRows = append([][]string{headers}, rows...)
} else {
r.allRows = rows
}
for _, row := range r.allRows {
for i, cell := range row {
cellLen := lipgloss.Width(cell)
// Header or first row. Just add as is.
if len(r.columns) <= i {
r.columns = append(r.columns, resizerColumn{
index: i,
min: cellLen,
max: cellLen,
median: cellLen,
})
continue
}
r.columns[i].rows = append(r.columns[i].rows, row)
r.columns[i].min = min(r.columns[i].min, cellLen)
r.columns[i].max = max(r.columns[i].max, cellLen)
}
}
for j := range r.columns {
widths := make([]int, len(r.columns[j].rows))
for i, row := range r.columns[j].rows {
widths[i] = lipgloss.Width(row[j])
}
r.columns[j].median = median(widths)
}
return r
}
// optimizedWidths returns the optimized column widths and row heights.
func (r *resizer) optimizedWidths() (colWidths, rowHeights []int) {
if r.maxTotal() <= r.tableWidth {
return r.expandTableWidth()
}
return r.shrinkTableWidth()
}
// detectTableWidth detects the table width.
func (r *resizer) detectTableWidth() int {
return r.maxCharCount() + r.totalHorizontalPadding() + r.totalHorizontalBorder()
}
// expandTableWidth expands the table width.
func (r *resizer) expandTableWidth() (colWidths, rowHeights []int) {
colWidths = r.maxColumnWidths()
for {
totalWidth := sum(colWidths) + r.totalHorizontalBorder()
if totalWidth >= r.tableWidth {
break
}
shorterColumnIndex := 0
shorterColumnWidth := math.MaxInt32
for j, width := range colWidths {
if width == r.columns[j].fixedWidth {
continue
}
if width < shorterColumnWidth {
shorterColumnWidth = width
shorterColumnIndex = j
}
}
colWidths[shorterColumnIndex]++
}
rowHeights = r.expandRowHeigths(colWidths)
return
}
// shrinkTableWidth shrinks the table width.
func (r *resizer) shrinkTableWidth() (colWidths, rowHeights []int) {
colWidths = r.maxColumnWidths()
// Cut width of columns that are way too big.
shrinkBiggestColumns := func(veryBigOnly bool) {
for {
totalWidth := sum(colWidths) + r.totalHorizontalBorder()
if totalWidth <= r.tableWidth {
break
}
bigColumnIndex := -math.MaxInt32
bigColumnWidth := -math.MaxInt32
for j, width := range colWidths {
if width == r.columns[j].fixedWidth {
continue
}
if veryBigOnly {
if width >= (r.tableWidth/2) && width > bigColumnWidth { //nolint:mnd
bigColumnWidth = width
bigColumnIndex = j
}
} else {
if width > bigColumnWidth {
bigColumnWidth = width
bigColumnIndex = j
}
}
}
if bigColumnIndex < 0 || colWidths[bigColumnIndex] == 0 {
break
}
colWidths[bigColumnIndex]--
}
}
// Cut width of columns that differ the most from the median.
shrinkToMedian := func() {
for {
totalWidth := sum(colWidths) + r.totalHorizontalBorder()
if totalWidth <= r.tableWidth {
break
}
biggestDiffToMedian := -math.MaxInt32
biggestDiffToMedianIndex := -math.MaxInt32
for j, width := range colWidths {
if width == r.columns[j].fixedWidth {
continue
}
diffToMedian := width - r.columns[j].median
if diffToMedian > 0 && diffToMedian > biggestDiffToMedian {
biggestDiffToMedian = diffToMedian
biggestDiffToMedianIndex = j
}
}
if biggestDiffToMedianIndex <= 0 || colWidths[biggestDiffToMedianIndex] == 0 {
break
}
colWidths[biggestDiffToMedianIndex]--
}
}
shrinkBiggestColumns(true)
shrinkToMedian()
shrinkBiggestColumns(false)
return colWidths, r.expandRowHeigths(colWidths)
}
// expandRowHeigths expands the row heights.
func (r *resizer) expandRowHeigths(colWidths []int) (rowHeights []int) {
rowHeights = r.defaultRowHeights()
if !r.wrap {
return rowHeights
}
for i, row := range r.allRows {
for j, cell := range row {
height := r.detectContentHeight(cell, colWidths[j]-r.xPaddingForCol(j)) + r.xPaddingForCell(i, j)
if height > rowHeights[i] {
rowHeights[i] = height
}
}
}
return
}
// defaultRowHeights returns the default row heights.
func (r *resizer) defaultRowHeights() (rowHeights []int) {
rowHeights = make([]int, len(r.allRows))
for i := range rowHeights {
if i < len(r.rowHeights) {
rowHeights[i] = r.rowHeights[i]
}
if rowHeights[i] < 1 {
rowHeights[i] = 1
}
}
return
}
// maxColumnWidths returns the maximum column widths.
func (r *resizer) maxColumnWidths() []int {
maxColumnWidths := make([]int, len(r.columns))
for i, col := range r.columns {
if col.fixedWidth > 0 {
maxColumnWidths[i] = col.fixedWidth
} else {
maxColumnWidths[i] = col.max + r.xPaddingForCol(col.index)
}
}
return maxColumnWidths
}
// columnCount returns the column count.
func (r *resizer) columnCount() int {
return len(r.columns)
}
// maxCharCount returns the maximum character count.
func (r *resizer) maxCharCount() int {
var count int
for _, col := range r.columns {
if col.fixedWidth > 0 {
count += col.fixedWidth - r.xPaddingForCol(col.index)
} else {
count += col.max
}
}
return count
}
// maxTotal returns the maximum total width.
func (r *resizer) maxTotal() (maxTotal int) {
for j, column := range r.columns {
if column.fixedWidth > 0 {
maxTotal += column.fixedWidth
} else {
maxTotal += column.max + r.xPaddingForCol(j)
}
}
return
}
// totalHorizontalPadding returns the total padding.
func (r *resizer) totalHorizontalPadding() (totalHorizontalPadding int) {
for _, col := range r.columns {
totalHorizontalPadding += col.xPadding
}
return
}
// xPaddingForCol returns the horizontal padding for a column.
func (r *resizer) xPaddingForCol(j int) int {
if j >= len(r.columns) {
return 0
}
return r.columns[j].xPadding
}
// xPaddingForCell returns the horizontal padding for a cell.
func (r *resizer) xPaddingForCell(i, j int) int {
if i >= len(r.yPaddings) || j >= len(r.yPaddings[i]) {
return 0
}
return r.yPaddings[i][j]
}
// totalHorizontalBorder returns the total border.
func (r *resizer) totalHorizontalBorder() int {
return (r.columnCount() * r.borderPerCell()) + r.extraBorder()
}
// borderPerCell returns number of border chars per cell.
func (r *resizer) borderPerCell() int {
if r.borderColumn {
return 1
}
return 0
}
// extraBorder returns the number of the extra border char at the end of the table.
func (r *resizer) extraBorder() int {
if r.borderColumn {
return 1
}
return 0
}
// detectContentHeight detects the content height.
func (r *resizer) detectContentHeight(content string, width int) (height int) {
if width == 0 {
return 1
}
content = strings.ReplaceAll(content, "\r\n", "\n")
for _, line := range strings.Split(content, "\n") {
height += strings.Count(ansi.Wrap(line, width, ""), "\n") + 1
}
return
}

View File

@ -111,3 +111,19 @@ func (m *Filter) Rows() int {
return j
}
// dataToMatrix converts an object that implements the Data interface to a table.
func dataToMatrix(data Data) (rows [][]string) {
numRows := data.Rows()
numCols := data.Columns()
rows = make([][]string, numRows)
for i := 0; i < numRows; i++ {
rows[i] = make([]string, numCols)
for j := 0; j < numCols; j++ {
rows[i][j] = data.At(i, j)
}
}
return
}

View File

@ -61,6 +61,7 @@ type Table struct {
height int
useManualHeight bool
offset int
wrap bool
// widths tracks the width of each column.
widths []int
@ -83,6 +84,7 @@ func New() *Table {
borderLeft: true,
borderRight: true,
borderTop: true,
wrap: true,
data: NewStringData(),
}
}
@ -209,11 +211,20 @@ func (t *Table) Height(h int) *Table {
}
// Offset sets the table rendering offset.
//
// Warning: you may declare Offset only after setting Rows. Otherwise it will be
// ignored.
func (t *Table) Offset(o int) *Table {
t.offset = o
return t
}
// Wrap dictates whether or not the table content should wrap.
func (t *Table) Wrap(w bool) *Table {
t.wrap = w
return t
}
// String returns the table as a string.
func (t *Table) String() string {
hasHeaders := len(t.headers) > 0
@ -231,120 +242,8 @@ func (t *Table) String() string {
}
}
// Initialize the widths.
t.widths = make([]int, max(len(t.headers), t.data.Columns()))
t.heights = make([]int, btoi(hasHeaders)+t.data.Rows())
// The style function may affect width of the table. It's possible to set
// the StyleFunc after the headers and rows. Update the widths for a final
// time.
for i, cell := range t.headers {
t.widths[i] = max(t.widths[i], lipgloss.Width(t.style(HeaderRow, i).Render(cell)))
t.heights[0] = max(t.heights[0], lipgloss.Height(t.style(HeaderRow, i).Render(cell)))
}
for r := 0; r < t.data.Rows(); r++ {
for i := 0; i < t.data.Columns(); i++ {
cell := t.data.At(r, i)
rendered := t.style(r, i).Render(cell)
t.heights[r+btoi(hasHeaders)] = max(t.heights[r+btoi(hasHeaders)], lipgloss.Height(rendered))
t.widths[i] = max(t.widths[i], lipgloss.Width(rendered))
}
}
// Table Resizing Logic.
//
// Given a user defined table width, we must ensure the table is exactly that
// width. This must account for all borders, column, separators, and column
// data.
//
// In the case where the table is narrower than the specified table width,
// we simply expand the columns evenly to fit the width.
// For example, a table with 3 columns takes up 50 characters total, and the
// width specified is 80, we expand each column by 10 characters, adding 30
// to the total width.
//
// In the case where the table is wider than the specified table width, we
// _could_ simply shrink the columns evenly but this would result in data
// being truncated (perhaps unnecessarily). The naive approach could result
// in very poor cropping of the table data. So, instead of shrinking columns
// evenly, we calculate the median non-whitespace length of each column, and
// shrink the columns based on the largest median.
//
// For example,
// ┌──────┬───────────────┬──────────┐
// │ Name │ Age of Person │ Location │
// ├──────┼───────────────┼──────────┤
// │ Kini │ 40 │ New York │
// │ Eli │ 30 │ London │
// │ Iris │ 20 │ Paris │
// └──────┴───────────────┴──────────┘
//
// Median non-whitespace length vs column width of each column:
//
// Name: 4 / 5
// Age of Person: 2 / 15
// Location: 6 / 10
//
// The biggest difference is 15 - 2, so we can shrink the 2nd column by 13.
width := t.computeWidth()
if width < t.width && t.width > 0 {
// Table is too narrow, expand the columns evenly until it reaches the
// desired width.
var i int
for width < t.width {
t.widths[i]++
width++
i = (i + 1) % len(t.widths)
}
} else if width > t.width && t.width > 0 {
// Table is too wide, calculate the median non-whitespace length of each
// column, and shrink the columns based on the largest difference.
columnMedians := make([]int, len(t.widths))
for c := range t.widths {
trimmedWidth := make([]int, t.data.Rows())
for r := 0; r < t.data.Rows(); r++ {
renderedCell := t.style(r+btoi(hasHeaders), c).Render(t.data.At(r, c))
nonWhitespaceChars := lipgloss.Width(strings.TrimRight(renderedCell, " "))
trimmedWidth[r] = nonWhitespaceChars + 1
}
columnMedians[c] = median(trimmedWidth)
}
// Find the biggest differences between the median and the column width.
// Shrink the columns based on the largest difference.
differences := make([]int, len(t.widths))
for i := range t.widths {
differences[i] = t.widths[i] - columnMedians[i]
}
for width > t.width {
index, _ := largest(differences)
if differences[index] < 1 {
break
}
shrink := min(differences[index], width-t.width)
t.widths[index] -= shrink
width -= shrink
differences[index] = 0
}
// Table is still too wide, begin shrinking the columns based on the
// largest column.
for width > t.width {
index, _ := largest(t.widths)
if t.widths[index] < 1 {
break
}
t.widths[index]--
width--
}
}
// Do all the sizing calculations for width and height.
t.resize()
var sb strings.Builder
@ -393,15 +292,6 @@ func (t *Table) String() string {
Render(sb.String())
}
// computeWidth computes the width of the table in it's current configuration.
func (t *Table) computeWidth() int {
width := sum(t.widths) + btoi(t.borderLeft) + btoi(t.borderRight)
if t.borderColumn {
width += len(t.widths) - 1
}
return width
}
// computeHeight computes the height of the table in it's current configuration.
func (t *Table) computeHeight() int {
hasHeaders := len(t.headers) > 0
@ -553,13 +443,17 @@ func (t *Table) constructRow(index int, isOverflow bool) string {
}
cellStyle := t.style(index, c)
if !t.wrap {
length := (cellWidth * height) - cellStyle.GetHorizontalPadding()
cell = ansi.Truncate(cell, length, "…")
}
cells = append(cells, cellStyle.
// Account for the margins in the cell sizing.
Height(height-cellStyle.GetVerticalMargins()).
MaxHeight(height).
Width(t.widths[c]-cellStyle.GetHorizontalMargins()).
MaxWidth(t.widths[c]).
Render(ansi.Truncate(cell, cellWidth*height, "…")))
Render(cell))
if c < t.data.Columns()-1 && t.borderColumn {
cells = append(cells, left)

View File

@ -20,7 +20,7 @@ func max(a, b int) int { //nolint:predeclared
return b
}
// min returns the greater of two integers.
// min returns the smaller of two integers.
func min(a, b int) int { //nolint:predeclared
if a < b {
return a
@ -45,20 +45,8 @@ func median(n []int) int {
return 0
}
if len(n)%2 == 0 {
h := len(n) / 2 //nolint:gomnd
return (n[h-1] + n[h]) / 2 //nolint:gomnd
h := len(n) / 2 //nolint:mnd
return (n[h-1] + n[h]) / 2 //nolint:mnd
}
return n[len(n)/2]
}
// largest returns the largest element and it's index from a slice of integers.
func largest(n []int) (int, int) { //nolint:unparam
var largest, index int
for i, e := range n {
if n[i] > n[index] {
largest = e
index = i
}
}
return index, largest
}

View File

@ -15,20 +15,27 @@ issues:
linters:
enable:
- bodyclose
- dupl
- exportloopref
- exhaustive
- goconst
- godot
- godox
- gofumpt
- goimports
- gomoddirectives
- goprintffuncname
- gosec
- misspell
- nakedret
- nestif
- nilerr
- noctx
- nolintlint
- prealloc
- revive
- rowserrcheck
- sqlclosecheck
- tparallel
- unconvert
- unparam
- whitespace
- wrapcheck

View File

@ -1,61 +1,151 @@
package log
import (
"bytes"
"encoding/json"
"fmt"
"time"
)
func (l *Logger) jsonFormatter(keyvals ...interface{}) {
m := make(map[string]interface{}, len(keyvals)/2)
for i := 0; i < len(keyvals); i += 2 {
switch keyvals[i] {
case TimestampKey:
if t, ok := keyvals[i+1].(time.Time); ok {
m[TimestampKey] = t.Format(l.timeFormat)
}
case LevelKey:
if level, ok := keyvals[i+1].(Level); ok {
m[LevelKey] = level.String()
}
case CallerKey:
if caller, ok := keyvals[i+1].(string); ok {
m[CallerKey] = caller
}
case PrefixKey:
if prefix, ok := keyvals[i+1].(string); ok {
m[PrefixKey] = prefix
}
case MessageKey:
if msg := keyvals[i+1]; msg != nil {
m[MessageKey] = fmt.Sprint(msg)
}
jw := &jsonWriter{w: &l.b}
jw.start()
i := 0
for i < len(keyvals) {
switch kv := keyvals[i].(type) {
case slogAttr:
l.jsonFormatterRoot(jw, kv.Key, kv.Value)
i++
default:
var (
key string
val interface{}
)
switch k := keyvals[i].(type) {
case fmt.Stringer:
key = k.String()
case error:
key = k.Error()
default:
key = fmt.Sprint(k)
if i+1 < len(keyvals) {
l.jsonFormatterRoot(jw, keyvals[i], keyvals[i+1])
}
switch v := keyvals[i+1].(type) {
case error:
val = v.Error()
case fmt.Stringer:
val = v.String()
default:
val = v
}
m[key] = val
i += 2
}
}
e := json.NewEncoder(&l.b)
e.SetEscapeHTML(false)
_ = e.Encode(m)
jw.end()
l.b.WriteRune('\n')
}
func (l *Logger) jsonFormatterRoot(jw *jsonWriter, key, value any) {
switch key {
case TimestampKey:
if t, ok := value.(time.Time); ok {
jw.objectItem(TimestampKey, t.Format(l.timeFormat))
}
case LevelKey:
if level, ok := value.(Level); ok {
jw.objectItem(LevelKey, level.String())
}
case CallerKey:
if caller, ok := value.(string); ok {
jw.objectItem(CallerKey, caller)
}
case PrefixKey:
if prefix, ok := value.(string); ok {
jw.objectItem(PrefixKey, prefix)
}
case MessageKey:
if msg := value; msg != nil {
jw.objectItem(MessageKey, fmt.Sprint(msg))
}
default:
l.jsonFormatterItem(jw, key, value)
}
}
func (l *Logger) jsonFormatterItem(jw *jsonWriter, key, value any) {
switch k := key.(type) {
case fmt.Stringer:
jw.objectKey(k.String())
case error:
jw.objectKey(k.Error())
default:
jw.objectKey(fmt.Sprint(k))
}
switch v := value.(type) {
case error:
jw.objectValue(v.Error())
case slogLogValuer:
l.writeSlogValue(jw, v.LogValue())
case slogValue:
l.writeSlogValue(jw, v.Resolve())
case fmt.Stringer:
jw.objectValue(v.String())
default:
jw.objectValue(v)
}
}
func (l *Logger) writeSlogValue(jw *jsonWriter, v slogValue) {
switch v.Kind() { //nolint:exhaustive
case slogKindGroup:
jw.start()
for _, attr := range v.Group() {
l.jsonFormatterItem(jw, attr.Key, attr.Value)
}
jw.end()
default:
jw.objectValue(v.Any())
}
}
type jsonWriter struct {
w *bytes.Buffer
d int
}
func (w *jsonWriter) start() {
w.w.WriteRune('{')
w.d = 0
}
func (w *jsonWriter) end() {
w.w.WriteRune('}')
}
func (w *jsonWriter) objectItem(key string, value any) {
w.objectKey(key)
w.objectValue(value)
}
func (w *jsonWriter) objectKey(key string) {
if w.d > 0 {
w.w.WriteRune(',')
}
w.d++
pos := w.w.Len()
err := w.writeEncoded(key)
if err != nil {
w.w.Truncate(pos)
w.w.WriteString(`"invalid key"`)
}
w.w.WriteRune(':')
}
func (w *jsonWriter) objectValue(value any) {
pos := w.w.Len()
err := w.writeEncoded(value)
if err != nil {
w.w.Truncate(pos)
w.w.WriteString(`"invalid value"`)
}
}
func (w *jsonWriter) writeEncoded(v any) error {
e := json.NewEncoder(w.w)
e.SetEscapeHTML(false)
if err := e.Encode(v); err != nil {
return fmt.Errorf("failed to encode value: %w", err)
}
// trailing \n added by json.Encode
b := w.w.Bytes()
if len(b) > 0 && b[len(b)-1] == '\n' {
w.w.Truncate(w.w.Len() - 1)
}
return nil
}

View File

@ -8,7 +8,7 @@ import (
)
// Level is a logging level.
type Level int32
type Level int
const (
// DebugLevel is the debug level.
@ -22,12 +22,12 @@ const (
// FatalLevel is the fatal level.
FatalLevel Level = 12
// noLevel is used with log.Print.
noLevel Level = math.MaxInt32
noLevel Level = math.MaxInt
)
// String returns the string representation of the level.
func (l Level) String() string {
switch l {
switch l { //nolint:exhaustive
case DebugLevel:
return "debug"
case InfoLevel:

View File

@ -1,15 +0,0 @@
//go:build go1.21
// +build go1.21
package log
import "log/slog"
// fromSlogLevel converts slog.Level to log.Level.
var fromSlogLevel = map[slog.Level]Level{
slog.LevelDebug: DebugLevel,
slog.LevelInfo: InfoLevel,
slog.LevelWarn: WarnLevel,
slog.LevelError: ErrorLevel,
slog.Level(12): FatalLevel,
}

View File

@ -1,15 +0,0 @@
//go:build !go1.21
// +build !go1.21
package log
import "golang.org/x/exp/slog"
// fromSlogLevel converts slog.Level to log.Level.
var fromSlogLevel = map[slog.Level]Level{
slog.LevelDebug: DebugLevel,
slog.LevelInfo: InfoLevel,
slog.LevelWarn: WarnLevel,
slog.LevelError: ErrorLevel,
slog.Level(12): FatalLevel,
}

View File

@ -30,7 +30,7 @@ type Logger struct {
isDiscard uint32
level int32
level int64
prefix string
timeFunc TimeFunction
timeFormat string
@ -59,7 +59,7 @@ func (l *Logger) Log(level Level, msg interface{}, keyvals ...interface{}) {
}
// check if the level is allowed
if atomic.LoadInt32(&l.level) > int32(level) {
if atomic.LoadInt64(&l.level) > int64(level) {
return
}
@ -129,6 +129,8 @@ func (l *Logger) handle(level Level, ts time.Time, frames []runtime.Frame, msg i
l.logfmtFormatter(kvs...)
case JSONFormatter:
l.jsonFormatter(kvs...)
case TextFormatter:
fallthrough
default:
l.textFormatter(kvs...)
}
@ -234,7 +236,7 @@ func (l *Logger) GetLevel() Level {
func (l *Logger) SetLevel(level Level) {
l.mu.Lock()
defer l.mu.Unlock()
atomic.StoreInt32(&l.level, int32(level))
atomic.StoreInt64(&l.level, int64(level))
}
// GetPrefix returns the current prefix.
@ -334,7 +336,8 @@ func (l *Logger) With(keyvals ...interface{}) *Logger {
sl.b = bytes.Buffer{}
sl.mu = &sync.RWMutex{}
sl.helpers = &sync.Map{}
sl.fields = append(l.fields, keyvals...)
sl.fields = append(make([]interface{}, 0, len(l.fields)+len(keyvals)), l.fields...)
sl.fields = append(sl.fields, keyvals...)
sl.styles = &st
return &sl
}

View File

@ -10,11 +10,20 @@ import (
"sync/atomic"
)
// type aliases for slog.
type (
slogAttr = slog.Attr
slogValue = slog.Value
slogLogValuer = slog.LogValuer
)
const slogKindGroup = slog.KindGroup
// Enabled reports whether the logger is enabled for the given level.
//
// Implements slog.Handler.
func (l *Logger) Enabled(_ context.Context, level slog.Level) bool {
return atomic.LoadInt32(&l.level) <= int32(fromSlogLevel[level])
return atomic.LoadInt64(&l.level) <= int64(level)
}
// Handle handles the Record. It will only be called if Enabled returns true.
@ -27,13 +36,13 @@ func (l *Logger) Handle(ctx context.Context, record slog.Record) error {
fields := make([]interface{}, 0, record.NumAttrs()*2)
record.Attrs(func(a slog.Attr) bool {
fields = append(fields, a.Key, a.Value.String())
fields = append(fields, a.Key, a.Value)
return true
})
// Get the caller frame using the record's PC.
frames := runtime.CallersFrames([]uintptr{record.PC})
frame, _ := frames.Next()
l.handle(fromSlogLevel[record.Level], l.timeFunc(record.Time), []runtime.Frame{frame}, record.Message, fields...)
l.handle(Level(record.Level), l.timeFunc(record.Time), []runtime.Frame{frame}, record.Message, fields...)
return nil
}

View File

@ -11,11 +11,20 @@ import (
"golang.org/x/exp/slog"
)
// type alises for slog.
type (
slogAttr = slog.Attr
slogValue = slog.Value
slogLogValuer = slog.LogValuer
)
const slogKindGroup = slog.KindGroup
// Enabled reports whether the logger is enabled for the given level.
//
// Implements slog.Handler.
func (l *Logger) Enabled(_ context.Context, level slog.Level) bool {
return atomic.LoadInt32(&l.level) <= int32(fromSlogLevel[level])
return atomic.LoadInt64(&l.level) <= int64(level)
}
// Handle handles the Record. It will only be called if Enabled returns true.
@ -24,13 +33,13 @@ func (l *Logger) Enabled(_ context.Context, level slog.Level) bool {
func (l *Logger) Handle(_ context.Context, record slog.Record) error {
fields := make([]interface{}, 0, record.NumAttrs()*2)
record.Attrs(func(a slog.Attr) bool {
fields = append(fields, a.Key, a.Value.String())
fields = append(fields, a.Key, a.Value)
return true
})
// Get the caller frame using the record's PC.
frames := runtime.CallersFrames([]uintptr{record.PC})
frame, _ := frames.Next()
l.handle(fromSlogLevel[record.Level], l.timeFunc(record.Time), []runtime.Frame{frame}, record.Message, fields...)
l.handle(Level(record.Level), l.timeFunc(record.Time), []runtime.Frame{frame}, record.Message, fields...)
return nil
}

View File

@ -7,6 +7,7 @@ import (
"log"
"os"
"sync"
"sync/atomic"
"time"
"github.com/muesli/termenv"
@ -17,25 +18,27 @@ var (
registry = sync.Map{}
// defaultLogger is the default global logger instance.
defaultLogger atomic.Pointer[Logger]
defaultLoggerOnce sync.Once
defaultLogger *Logger
)
// Default returns the default logger. The default logger comes with timestamp enabled.
func Default() *Logger {
defaultLoggerOnce.Do(func() {
if defaultLogger != nil {
// already set via SetDefault.
return
}
defaultLogger = NewWithOptions(os.Stderr, Options{ReportTimestamp: true})
})
return defaultLogger
dl := defaultLogger.Load()
if dl == nil {
defaultLoggerOnce.Do(func() {
defaultLogger.CompareAndSwap(
nil, NewWithOptions(os.Stderr, Options{ReportTimestamp: true}),
)
})
dl = defaultLogger.Load()
}
return dl
}
// SetDefault sets the default global logger.
func SetDefault(logger *Logger) {
defaultLogger = logger
defaultLogger.Store(logger)
}
// New returns a new logger with the default options.
@ -49,7 +52,7 @@ func NewWithOptions(w io.Writer, o Options) *Logger {
b: bytes.Buffer{},
mu: &sync.RWMutex{},
helpers: &sync.Map{},
level: int32(o.Level),
level: int64(o.Level),
reportTimestamp: o.ReportTimestamp,
reportCaller: o.ReportCaller,
prefix: o.Prefix,

View File

@ -14,7 +14,7 @@ func (l *stdLogWriter) Write(p []byte) (n int, err error) {
str := strings.TrimSuffix(string(p), "\n")
if l.opt != nil {
switch l.opt.ForceLevel {
switch l.opt.ForceLevel { //nolint:exhaustive
case DebugLevel:
l.l.Debug(str)
case InfoLevel:

View File

@ -21,7 +21,7 @@ func (l *Logger) writeIndent(w io.Writer, str string, indent string, newline boo
// kindly borrowed from hclog
for {
nl := strings.IndexByte(str, '\n')
if nl == -1 {
if nl == -1 { //nolint:nestif
if str != "" {
_, _ = w.Write([]byte(indent))
val := escapeStringForOutput(str, false)

View File

@ -178,7 +178,7 @@ func ansiToRGB(ansi uint32) (uint32, uint32, uint32) {
//
// r, g, b := hexToRGB(0x0000FF)
func hexToRGB(hex uint32) (uint32, uint32, uint32) {
return hex >> 16, hex >> 8 & 0xff, hex & 0xff
return hex >> 16 & 0xff, hex >> 8 & 0xff, hex & 0xff
}
// toRGBA converts an RGB 8-bit color values to 32-bit color values suitable

View File

@ -1,120 +0,0 @@
package ansi
import (
"bytes"
"strconv"
)
// CsiSequence represents a control sequence introducer (CSI) sequence.
//
// The sequence starts with a CSI sequence, CSI (0x9B) in a 8-bit environment
// or ESC [ (0x1B 0x5B) in a 7-bit environment, followed by any number of
// parameters in the range of 0x30-0x3F, then by any number of intermediate
// byte in the range of 0x20-0x2F, then finally with a single final byte in the
// range of 0x20-0x7E.
//
// CSI P..P I..I F
//
// See ECMA-48 § 5.4.
type CsiSequence struct {
// Params contains the raw parameters of the sequence.
// This is a slice of integers, where each integer is a 32-bit integer
// containing the parameter value in the lower 31 bits and a flag in the
// most significant bit indicating whether there are more sub-parameters.
Params []Parameter
// Cmd contains the raw command of the sequence.
// The command is a 32-bit integer containing the CSI command byte in the
// lower 8 bits, the private marker in the next 8 bits, and the intermediate
// byte in the next 8 bits.
//
// CSI ? u
//
// Is represented as:
//
// 'u' | '?' << 8
Cmd Command
}
var _ Sequence = CsiSequence{}
// Clone returns a deep copy of the CSI sequence.
func (s CsiSequence) Clone() Sequence {
return CsiSequence{
Params: append([]Parameter(nil), s.Params...),
Cmd: s.Cmd,
}
}
// Marker returns the marker byte of the CSI sequence.
// This is always gonna be one of the following '<' '=' '>' '?' and in the
// range of 0x3C-0x3F.
// Zero is returned if the sequence does not have a marker.
func (s CsiSequence) Marker() int {
return s.Cmd.Marker()
}
// Intermediate returns the intermediate byte of the CSI sequence.
// An intermediate byte is in the range of 0x20-0x2F. This includes these
// characters from ' ', '!', '"', '#', '$', '%', '&', ”', '(', ')', '*', '+',
// ',', '-', '.', '/'.
// Zero is returned if the sequence does not have an intermediate byte.
func (s CsiSequence) Intermediate() int {
return s.Cmd.Intermediate()
}
// Command returns the command byte of the CSI sequence.
func (s CsiSequence) Command() int {
return s.Cmd.Command()
}
// Param is a helper that returns the parameter at the given index and falls
// back to the default value if the parameter is missing. If the index is out
// of bounds, it returns the default value and false.
func (s CsiSequence) Param(i, def int) (int, bool) {
if i < 0 || i >= len(s.Params) {
return def, false
}
return s.Params[i].Param(def), true
}
// String returns a string representation of the sequence.
// The string will always be in the 7-bit format i.e (ESC [ P..P I..I F).
func (s CsiSequence) String() string {
return s.buffer().String()
}
// buffer returns a buffer containing the sequence.
func (s CsiSequence) buffer() *bytes.Buffer {
var b bytes.Buffer
b.WriteString("\x1b[")
if m := s.Marker(); m != 0 {
b.WriteByte(byte(m))
}
for i, p := range s.Params {
param := p.Param(-1)
if param >= 0 {
b.WriteString(strconv.Itoa(param))
}
if i < len(s.Params)-1 {
if p.HasMore() {
b.WriteByte(':')
} else {
b.WriteByte(';')
}
}
}
if i := s.Intermediate(); i != 0 {
b.WriteByte(byte(i))
}
if cmd := s.Command(); cmd != 0 {
b.WriteByte(byte(cmd))
}
return &b
}
// Bytes returns the byte representation of the sequence.
// The bytes will always be in the 7-bit format i.e (ESC [ P..P I..I F).
func (s CsiSequence) Bytes() []byte {
return s.buffer().Bytes()
}

View File

@ -14,7 +14,7 @@ import (
//
// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-PC-Style-Function-Keys
const (
RequestNameVersion = "\x1b[>0q"
RequestNameVersion = "\x1b[>q"
XTVERSION = RequestNameVersion
)
@ -24,6 +24,7 @@ const (
// DCS > | text ST
//
// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-PC-Style-Function-Keys
//
// Deprecated: use [RequestNameVersion] instead.
const RequestXTVersion = RequestNameVersion
@ -40,7 +41,7 @@ const RequestXTVersion = RequestNameVersion
// See https://vt100.net/docs/vt510-rm/DA1.html
func PrimaryDeviceAttributes(attrs ...int) string {
if len(attrs) == 0 {
return "\x1b[c"
return RequestPrimaryDeviceAttributes
} else if len(attrs) == 1 && attrs[0] == 0 {
return "\x1b[0c"
}
@ -75,7 +76,7 @@ const RequestPrimaryDeviceAttributes = "\x1b[c"
// See https://vt100.net/docs/vt510-rm/DA2.html
func SecondaryDeviceAttributes(attrs ...int) string {
if len(attrs) == 0 {
return "\x1b[>c"
return RequestSecondaryDeviceAttributes
}
as := make([]string, len(attrs))
@ -90,6 +91,14 @@ func DA2(attrs ...int) string {
return SecondaryDeviceAttributes(attrs...)
}
// RequestSecondaryDeviceAttributes is a control sequence that requests the
// terminal's secondary device attributes (DA2).
//
// CSI > c
//
// See https://vt100.net/docs/vt510-rm/DA2.html
const RequestSecondaryDeviceAttributes = "\x1b[>c"
// TertiaryDeviceAttributes (DA3) is a control sequence that reports the
// terminal's tertiary device attributes.
//
@ -106,7 +115,7 @@ func DA2(attrs ...int) string {
func TertiaryDeviceAttributes(unitID string) string {
switch unitID {
case "":
return "\x1b[=c"
return RequestTertiaryDeviceAttributes
case "0":
return "\x1b[=0c"
}
@ -118,3 +127,11 @@ func TertiaryDeviceAttributes(unitID string) string {
func DA3(unitID string) string {
return TertiaryDeviceAttributes(unitID)
}
// RequestTertiaryDeviceAttributes is a control sequence that requests the
// terminal's tertiary device attributes (DA3).
//
// CSI = c
//
// See https://vt100.net/docs/vt510-rm/DA3.html
const RequestTertiaryDeviceAttributes = "\x1b[=c"

View File

@ -36,6 +36,8 @@ const (
//
// Where Pl is the line number and Pc is the column number.
// See: https://vt100.net/docs/vt510-rm/CPR.html
//
// Deprecated: use [RequestCursorPositionReport] instead.
const RequestCursorPosition = "\x1b[6n"
// RequestExtendedCursorPosition (DECXCPR) is a sequence for requesting the
@ -51,6 +53,8 @@ const RequestCursorPosition = "\x1b[6n"
// Where Pl is the line number, Pc is the column number, and Pp is the page
// number.
// See: https://vt100.net/docs/vt510-rm/DECXCPR.html
//
// Deprecated: use [RequestExtendedCursorPositionReport] instead.
const RequestExtendedCursorPosition = "\x1b[?6n"
// CursorUp (CUU) returns a sequence for moving the cursor up n cells.

View File

@ -1,133 +0,0 @@
package ansi
import (
"bytes"
"strconv"
"strings"
)
// DcsSequence represents a Device Control String (DCS) escape sequence.
//
// The DCS sequence is used to send device control strings to the terminal. The
// sequence starts with the C1 control code character DCS (0x9B) or ESC P in
// 7-bit environments, followed by parameter bytes, intermediate bytes, a
// command byte, followed by data bytes, and ends with the C1 control code
// character ST (0x9C) or ESC \ in 7-bit environments.
//
// This follows the parameter string format.
// See ECMA-48 § 5.4.1
type DcsSequence struct {
// Params contains the raw parameters of the sequence.
// This is a slice of integers, where each integer is a 32-bit integer
// containing the parameter value in the lower 31 bits and a flag in the
// most significant bit indicating whether there are more sub-parameters.
Params []Parameter
// Data contains the string raw data of the sequence.
// This is the data between the final byte and the escape sequence terminator.
Data []byte
// Cmd contains the raw command of the sequence.
// The command is a 32-bit integer containing the DCS command byte in the
// lower 8 bits, the private marker in the next 8 bits, and the intermediate
// byte in the next 8 bits.
//
// DCS > 0 ; 1 $ r <data> ST
//
// Is represented as:
//
// 'r' | '>' << 8 | '$' << 16
Cmd Command
}
var _ Sequence = DcsSequence{}
// Clone returns a deep copy of the DCS sequence.
func (s DcsSequence) Clone() Sequence {
return DcsSequence{
Params: append([]Parameter(nil), s.Params...),
Data: append([]byte(nil), s.Data...),
Cmd: s.Cmd,
}
}
// Split returns a slice of data split by the semicolon.
func (s DcsSequence) Split() []string {
return strings.Split(string(s.Data), ";")
}
// Marker returns the marker byte of the DCS sequence.
// This is always gonna be one of the following '<' '=' '>' '?' and in the
// range of 0x3C-0x3F.
// Zero is returned if the sequence does not have a marker.
func (s DcsSequence) Marker() int {
return s.Cmd.Marker()
}
// Intermediate returns the intermediate byte of the DCS sequence.
// An intermediate byte is in the range of 0x20-0x2F. This includes these
// characters from ' ', '!', '"', '#', '$', '%', '&', ”', '(', ')', '*', '+',
// ',', '-', '.', '/'.
// Zero is returned if the sequence does not have an intermediate byte.
func (s DcsSequence) Intermediate() int {
return s.Cmd.Intermediate()
}
// Command returns the command byte of the CSI sequence.
func (s DcsSequence) Command() int {
return s.Cmd.Command()
}
// Param is a helper that returns the parameter at the given index and falls
// back to the default value if the parameter is missing. If the index is out
// of bounds, it returns the default value and false.
func (s DcsSequence) Param(i, def int) (int, bool) {
if i < 0 || i >= len(s.Params) {
return def, false
}
return s.Params[i].Param(def), true
}
// String returns a string representation of the sequence.
// The string will always be in the 7-bit format i.e (ESC P p..p i..i f <data> ESC \).
func (s DcsSequence) String() string {
return s.buffer().String()
}
// buffer returns a buffer containing the sequence.
func (s DcsSequence) buffer() *bytes.Buffer {
var b bytes.Buffer
b.WriteString("\x1bP")
if m := s.Marker(); m != 0 {
b.WriteByte(byte(m))
}
for i, p := range s.Params {
param := p.Param(-1)
if param >= 0 {
b.WriteString(strconv.Itoa(param))
}
if i < len(s.Params)-1 {
if p.HasMore() {
b.WriteByte(':')
} else {
b.WriteByte(';')
}
}
}
if i := s.Intermediate(); i != 0 {
b.WriteByte(byte(i))
}
if cmd := s.Command(); cmd != 0 {
b.WriteByte(byte(cmd))
}
b.Write(s.Data)
b.WriteByte(ESC)
b.WriteByte('\\')
return &b
}
// Bytes returns the byte representation of the sequence.
// The bytes will always be in the 7-bit format i.e (ESC P p..p i..i F <data> ESC \).
func (s DcsSequence) Bytes() []byte {
return s.buffer().Bytes()
}

199
vendor/github.com/charmbracelet/x/ansi/graphics.go generated vendored Normal file
View File

@ -0,0 +1,199 @@
package ansi
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"image"
"io"
"os"
"strings"
"github.com/charmbracelet/x/ansi/kitty"
)
// KittyGraphics returns a sequence that encodes the given image in the Kitty
// graphics protocol.
//
// APC G [comma separated options] ; [base64 encoded payload] ST
//
// See https://sw.kovidgoyal.net/kitty/graphics-protocol/
func KittyGraphics(payload []byte, opts ...string) string {
var buf bytes.Buffer
buf.WriteString("\x1b_G")
buf.WriteString(strings.Join(opts, ","))
if len(payload) > 0 {
buf.WriteString(";")
buf.Write(payload)
}
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
}

18
vendor/github.com/charmbracelet/x/ansi/iterm2.go generated vendored Normal file
View File

@ -0,0 +1,18 @@
package ansi
import "fmt"
// ITerm2 returns a sequence that uses the iTerm2 proprietary protocol. Use the
// iterm2 package for a more convenient API.
//
// OSC 1337 ; key = value ST
//
// Example:
//
// ITerm2(iterm2.File{...})
//
// See https://iterm2.com/documentation-escape-codes.html
// See https://iterm2.com/documentation-images.html
func ITerm2(data any) string {
return "\x1b]1337;" + fmt.Sprint(data) + "\x07"
}

View File

@ -72,7 +72,7 @@ func PushKittyKeyboard(flags int) string {
// Keyboard stack to disable the protocol.
//
// This is equivalent to PushKittyKeyboard(0).
const DisableKittyKeyboard = "\x1b[>0u"
const DisableKittyKeyboard = "\x1b[>u"
// PopKittyKeyboard returns a sequence to pop n number of flags from the
// terminal Kitty Keyboard stack.

View File

@ -0,0 +1,85 @@
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

@ -0,0 +1,64 @@
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

@ -0,0 +1,414 @@
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',
}

367
vendor/github.com/charmbracelet/x/ansi/kitty/options.go generated vendored Normal file
View File

@ -0,0 +1,367 @@
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
}

172
vendor/github.com/charmbracelet/x/ansi/method.go generated vendored Normal file
View File

@ -0,0 +1,172 @@
package ansi
// Method is a type that represents the how the renderer should calculate the
// display width of cells.
type Method uint8
// Display width modes.
const (
WcWidth Method = iota
GraphemeWidth
)
// StringWidth returns the width of a string in cells. This is the number of
// cells that the string will occupy when printed in a terminal. ANSI escape
// codes are ignored and wide characters (such as East Asians and emojis) are
// accounted for.
func (m Method) StringWidth(s string) int {
return stringWidth(m, s)
}
// Truncate truncates a string to a given length, adding a tail to the end if
// the string is longer than the given length. 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).
func (m Method) Truncate(s string, length int, tail string) string {
return truncate(m, s, length, tail)
}
// TruncateLeft truncates a string to a given length, adding a prefix to the
// beginning if the string is longer than the given length. 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).
func (m Method) TruncateLeft(s string, length int, prefix string) string {
return truncateLeft(m, s, length, prefix)
}
// 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.
func (m Method) Cut(s string, left, right int) string {
return cut(m, s, left, right)
}
// Hardwrap wraps a string or a block of text to a given line length, breaking
// word boundaries. This will preserve ANSI escape codes and will account for
// wide-characters in the string.
// When preserveSpace is true, spaces at the beginning of a line will be
// preserved.
// This treats the text as a sequence of graphemes.
func (m Method) Hardwrap(s string, length int, preserveSpace bool) string {
return hardwrap(m, s, length, preserveSpace)
}
// Wordwrap wraps a string or a block of text to a given line length, not
// breaking word boundaries. This will preserve ANSI escape codes and will
// account for wide-characters in the string.
// The breakpoints string is a list of characters that are considered
// breakpoints for word wrapping. A hyphen (-) is always considered a
// breakpoint.
//
// Note: breakpoints must be a string of 1-cell wide rune characters.
func (m Method) Wordwrap(s string, length int, breakpoints string) string {
return wordwrap(m, s, length, breakpoints)
}
// Wrap wraps a string or a block of text to a given line length, breaking word
// boundaries if necessary. This will preserve ANSI escape codes and will
// account for wide-characters in the string. The breakpoints string is a list
// of characters that are considered breakpoints for word wrapping. A hyphen
// (-) is always considered a breakpoint.
//
// Note: breakpoints must be a string of 1-cell wide rune characters.
func (m Method) Wrap(s string, length int, breakpoints string) string {
return wrap(m, s, length, breakpoints)
}
// DecodeSequence decodes the first ANSI escape sequence or a printable
// grapheme from the given data. It returns the sequence slice, the number of
// bytes read, the cell width for each sequence, and the new state.
//
// The cell width will always be 0 for control and escape sequences, 1 for
// ASCII printable characters, and the number of cells other Unicode characters
// occupy. It uses the uniseg package to calculate the width of Unicode
// graphemes and characters. This means it will always do grapheme clustering
// (mode 2027).
//
// Passing a non-nil [*Parser] as the last argument will allow the decoder to
// collect sequence parameters, data, and commands. The parser cmd will have
// the packed command value that contains intermediate and prefix characters.
// In the case of a OSC sequence, the cmd will be the OSC command number. Use
// [Cmd] and [Param] types to unpack command intermediates and prefixes as well
// as parameters.
//
// Zero [Cmd] means the CSI, DCS, or ESC sequence is invalid. Moreover, checking the
// validity of other data sequences, OSC, DCS, etc, will require checking for
// the returned sequence terminator bytes such as ST (ESC \\) and BEL).
//
// We store the command byte in [Cmd] in the most significant byte, the
// prefix byte in the next byte, and the intermediate byte in the least
// significant byte. This is done to avoid using a struct to store the command
// and its intermediates and prefixes. The command byte is always the least
// significant byte i.e. [Cmd & 0xff]. Use the [Cmd] type to unpack the
// command, intermediate, and prefix bytes. Note that we only collect the last
// prefix character and intermediate byte.
//
// The [p.Params] slice will contain the parameters of the sequence. Any
// sub-parameter will have the [parser.HasMoreFlag] set. Use the [Param] type
// to unpack the parameters.
//
// Example:
//
// var state byte // the initial state is always zero [NormalState]
// p := NewParser(32, 1024) // create a new parser with a 32 params buffer and 1024 data buffer (optional)
// input := []byte("\x1b[31mHello, World!\x1b[0m")
// for len(input) > 0 {
// seq, width, n, newState := DecodeSequence(input, state, p)
// log.Printf("seq: %q, width: %d", seq, width)
// state = newState
// input = input[n:]
// }
func (m Method) DecodeSequence(data []byte, state byte, p *Parser) (seq []byte, width, n int, newState byte) {
return decodeSequence(m, data, state, p)
}
// DecodeSequenceInString decodes the first ANSI escape sequence or a printable
// grapheme from the given data. It returns the sequence slice, the number of
// bytes read, the cell width for each sequence, and the new state.
//
// The cell width will always be 0 for control and escape sequences, 1 for
// ASCII printable characters, and the number of cells other Unicode characters
// occupy. It uses the uniseg package to calculate the width of Unicode
// graphemes and characters. This means it will always do grapheme clustering
// (mode 2027).
//
// Passing a non-nil [*Parser] as the last argument will allow the decoder to
// collect sequence parameters, data, and commands. The parser cmd will have
// the packed command value that contains intermediate and prefix characters.
// In the case of a OSC sequence, the cmd will be the OSC command number. Use
// [Cmd] and [Param] types to unpack command intermediates and prefixes as well
// as parameters.
//
// Zero [Cmd] means the CSI, DCS, or ESC sequence is invalid. Moreover, checking the
// validity of other data sequences, OSC, DCS, etc, will require checking for
// the returned sequence terminator bytes such as ST (ESC \\) and BEL).
//
// We store the command byte in [Cmd] in the most significant byte, the
// prefix byte in the next byte, and the intermediate byte in the least
// significant byte. This is done to avoid using a struct to store the command
// and its intermediates and prefixes. The command byte is always the least
// significant byte i.e. [Cmd & 0xff]. Use the [Cmd] type to unpack the
// command, intermediate, and prefix bytes. Note that we only collect the last
// prefix character and intermediate byte.
//
// The [p.Params] slice will contain the parameters of the sequence. Any
// sub-parameter will have the [parser.HasMoreFlag] set. Use the [Param] type
// to unpack the parameters.
//
// Example:
//
// var state byte // the initial state is always zero [NormalState]
// p := NewParser(32, 1024) // create a new parser with a 32 params buffer and 1024 data buffer (optional)
// input := []byte("\x1b[31mHello, World!\x1b[0m")
// for len(input) > 0 {
// seq, width, n, newState := DecodeSequenceInString(input, state, p)
// log.Printf("seq: %q, width: %d", seq, width)
// state = newState
// input = input[n:]
// }
func (m Method) DecodeSequenceInString(data string, state byte, p *Parser) (seq string, width, n int, newState byte) {
return decodeSequence(m, data, state, p)
}

View File

@ -51,7 +51,8 @@ type Mode interface {
// SetMode (SM) 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 sequence will use the DEC format.
// If one of the modes is a [DECMode], the function will returns two escape
// sequences.
//
// ANSI format:
//
@ -74,7 +75,8 @@ func SM(modes ...Mode) string {
// ResetMode (RM) 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 sequence will use the DEC format.
// If one of the modes is a [DECMode], the function will returns two escape
// sequences.
//
// ANSI format:
//
@ -94,9 +96,9 @@ func RM(modes ...Mode) string {
return ResetMode(modes...)
}
func setMode(reset bool, modes ...Mode) string {
func setMode(reset bool, modes ...Mode) (s string) {
if len(modes) == 0 {
return ""
return
}
cmd := "h"
@ -113,21 +115,24 @@ func setMode(reset bool, modes ...Mode) string {
return seq + strconv.Itoa(modes[0].Mode()) + cmd
}
var dec bool
list := make([]string, len(modes))
for i, m := range modes {
list[i] = strconv.Itoa(m.Mode())
dec := make([]string, 0, len(modes)/2)
ansi := make([]string, 0, len(modes)/2)
for _, m := range modes {
switch m.(type) {
case DECMode:
dec = true
dec = append(dec, strconv.Itoa(m.Mode()))
case ANSIMode:
ansi = append(ansi, strconv.Itoa(m.Mode()))
}
}
if dec {
seq += "?"
if len(ansi) > 0 {
s += seq + strings.Join(ansi, ";") + cmd
}
return seq + strings.Join(list, ";") + cmd
if len(dec) > 0 {
s += seq + "?" + strings.Join(dec, ";") + cmd
}
return
}
// RequestMode (DECRQM) returns a sequence to request a mode from the terminal.

View File

@ -26,19 +26,28 @@ type MouseButton byte
// Other buttons are not supported.
const (
MouseNone MouseButton = iota
MouseLeft
MouseMiddle
MouseRight
MouseWheelUp
MouseWheelDown
MouseWheelLeft
MouseWheelRight
MouseBackward
MouseForward
MouseButton1
MouseButton2
MouseButton3
MouseButton4
MouseButton5
MouseButton6
MouseButton7
MouseButton8
MouseButton9
MouseButton10
MouseButton11
MouseRelease = MouseNone
MouseLeft = MouseButton1
MouseMiddle = MouseButton2
MouseRight = MouseButton3
MouseWheelUp = MouseButton4
MouseWheelDown = MouseButton5
MouseWheelLeft = MouseButton6
MouseWheelRight = MouseButton7
MouseBackward = MouseButton8
MouseForward = MouseButton9
MouseRelease = MouseNone
)
var mouseButtons = map[MouseButton]string{
@ -61,7 +70,7 @@ func (b MouseButton) String() string {
return mouseButtons[b]
}
// Button returns a byte representing a mouse button.
// EncodeMouseButton returns a byte representing a mouse button.
// The button is a bitmask of the following leftmost values:
//
// - The first two bits are the button number:
@ -85,7 +94,7 @@ func (b MouseButton) String() string {
//
// If button is [MouseNone], and motion is false, this returns a release event.
// If button is undefined, this function returns 0xff.
func (b MouseButton) Button(motion, shift, alt, ctrl bool) (m byte) {
func EncodeMouseButton(b MouseButton, motion, shift, alt, ctrl bool) (m byte) {
// mouse bit shifts
const (
bitShift = 0b0000_0100

View File

@ -2,8 +2,8 @@ package ansi
// Notify sends a desktop notification using iTerm's OSC 9.
//
// OSC 9 ; Mc ST
// OSC 9 ; Mc BEL
// OSC 9 ; Mc ST
// OSC 9 ; Mc BEL
//
// Where Mc is the notification body.
//

View File

@ -1,70 +0,0 @@
package ansi
import (
"bytes"
"strings"
)
// OscSequence represents an OSC sequence.
//
// The sequence starts with a OSC sequence, OSC (0x9D) in a 8-bit environment
// or ESC ] (0x1B 0x5D) in a 7-bit environment, followed by positive integer identifier,
// then by arbitrary data terminated by a ST (0x9C) in a 8-bit environment,
// ESC \ (0x1B 0x5C) in a 7-bit environment, or BEL (0x07) for backwards compatibility.
//
// OSC Ps ; Pt ST
// OSC Ps ; Pt BEL
//
// See ECMA-48 § 5.7.
type OscSequence struct {
// Data contains the raw data of the sequence including the identifier
// command.
Data []byte
// Cmd contains the raw command of the sequence.
Cmd int
}
var _ Sequence = OscSequence{}
// Clone returns a deep copy of the OSC sequence.
func (o OscSequence) Clone() Sequence {
return OscSequence{
Data: append([]byte(nil), o.Data...),
Cmd: o.Cmd,
}
}
// Split returns a slice of data split by the semicolon with the first element
// being the identifier command.
func (o OscSequence) Split() []string {
return strings.Split(string(o.Data), ";")
}
// Command returns the OSC command. This is always gonna be a positive integer
// that identifies the OSC sequence.
func (o OscSequence) Command() int {
return o.Cmd
}
// String returns the string representation of the OSC sequence.
// To be more compatible with different terminal, this will always return a
// 7-bit formatted sequence, terminated by BEL.
func (s OscSequence) String() string {
return s.buffer().String()
}
// Bytes returns the byte representation of the OSC sequence.
// To be more compatible with different terminal, this will always return a
// 7-bit formatted sequence, terminated by BEL.
func (s OscSequence) Bytes() []byte {
return s.buffer().Bytes()
}
func (s OscSequence) buffer() *bytes.Buffer {
var b bytes.Buffer
b.WriteString("\x1b]")
b.Write(s.Data)
b.WriteByte(BEL)
return &b
}

View File

@ -1,45 +0,0 @@
package ansi
import (
"bytes"
)
// Params parses and returns a list of control sequence parameters.
//
// Parameters are positive integers separated by semicolons. Empty parameters
// default to zero. Parameters can have sub-parameters separated by colons.
//
// Any non-parameter bytes are ignored. This includes bytes that are not in the
// range of 0x30-0x3B.
//
// See ECMA-48 § 5.4.1.
func Params(p []byte) [][]uint {
if len(p) == 0 {
return [][]uint{}
}
// Filter out non-parameter bytes i.e. non 0x30-0x3B.
p = bytes.TrimFunc(p, func(r rune) bool {
return r < 0x30 || r > 0x3B
})
parts := bytes.Split(p, []byte{';'})
params := make([][]uint, len(parts))
for i, part := range parts {
sparts := bytes.Split(part, []byte{':'})
params[i] = make([]uint, len(sparts))
for j, spart := range sparts {
params[i][j] = bytesToUint16(spart)
}
}
return params
}
func bytesToUint16(b []byte) uint {
var n uint
for _, c := range b {
n = n*10 + uint(c-'0')
}
return n
}

View File

@ -7,9 +7,6 @@ import (
"github.com/charmbracelet/x/ansi/parser"
)
// ParserDispatcher is a function that dispatches a sequence.
type ParserDispatcher func(Sequence)
// Parser represents a DEC ANSI compatible sequence parser.
//
// It uses a state machine to parse ANSI escape sequences and control
@ -20,8 +17,7 @@ type ParserDispatcher func(Sequence)
//
//go:generate go run ./gen.go
type Parser struct {
// the dispatch function to call when a sequence is complete
dispatcher ParserDispatcher
handler Handler
// params contains the raw parameters of the sequence.
// These parameters used when constructing CSI and DCS sequences.
@ -43,10 +39,10 @@ type Parser struct {
// number of rune bytes collected.
paramsLen int
// cmd contains the raw command along with the private marker and
// cmd contains the raw command along with the private prefix and
// intermediate bytes of the sequence.
// The first lower byte contains the command byte, the next byte contains
// the private marker, and the next byte contains the intermediate byte.
// the private prefix, and the next byte contains the intermediate byte.
//
// This is also used when collecting UTF-8 runes treating it as a slice of
// 4 bytes.
@ -56,24 +52,17 @@ type Parser struct {
state byte
}
// NewParser returns a new parser with an optional [ParserDispatcher].
// NewParser returns a new parser with the default settings.
// The [Parser] uses a default size of 32 for the parameters and 64KB for the
// data buffer. Use [Parser.SetParamsSize] and [Parser.SetDataSize] to set the
// size of the parameters and data buffer respectively.
func NewParser(d ParserDispatcher) *Parser {
func NewParser() *Parser {
p := new(Parser)
p.SetDispatcher(d)
p.SetParamsSize(parser.MaxParamsSize)
p.SetDataSize(1024 * 64) // 64KB data buffer
return p
}
// SetDispatcher sets the dispatcher function to call when a sequence is
// complete.
func (p *Parser) SetDispatcher(d ParserDispatcher) {
p.dispatcher = d
}
// SetParamsSize sets the size of the parameters buffer.
// This is used when constructing CSI and DCS sequences.
func (p *Parser) SetParamsSize(size int) {
@ -93,8 +82,8 @@ func (p *Parser) SetDataSize(size int) {
}
// Params returns the list of parsed packed parameters.
func (p *Parser) Params() []Parameter {
return unsafe.Slice((*Parameter)(unsafe.Pointer(&p.params[0])), p.paramsLen)
func (p *Parser) Params() Params {
return unsafe.Slice((*Param)(unsafe.Pointer(&p.params[0])), p.paramsLen)
}
// Param returns the parameter at the given index and falls back to the default
@ -104,12 +93,13 @@ func (p *Parser) Param(i, def int) (int, bool) {
if i < 0 || i >= p.paramsLen {
return def, false
}
return Parameter(p.params[i]).Param(def), true
return Param(p.params[i]).Param(def), true
}
// Cmd returns the packed command of the last dispatched sequence.
func (p *Parser) Cmd() Command {
return Command(p.cmd)
// Command returns the packed command of the last dispatched sequence. Use
// [Cmd] to unpack the command.
func (p *Parser) Command() int {
return p.cmd
}
// Rune returns the last dispatched sequence as a rune.
@ -122,6 +112,11 @@ func (p *Parser) Rune() rune {
return r
}
// Control returns the last dispatched sequence as a control code.
func (p *Parser) Control() byte {
return byte(p.cmd & 0xff)
}
// Data returns the raw data of the last dispatched sequence.
func (p *Parser) Data() []byte {
return p.data[:p.dataLen]
@ -183,12 +178,6 @@ func (p *Parser) collectRune(b byte) {
p.paramsLen++
}
func (p *Parser) dispatch(s Sequence) {
if p.dispatcher != nil {
p.dispatcher(s)
}
}
func (p *Parser) advanceUtf8(b byte) parser.Action {
// Collect UTF-8 rune bytes.
p.collectRune(b)
@ -204,7 +193,9 @@ func (p *Parser) advanceUtf8(b byte) parser.Action {
}
// We have enough bytes to decode the rune using unsafe
p.dispatch(Rune(p.Rune()))
if p.handler.Print != nil {
p.handler.Print(p.Rune())
}
p.state = parser.GroundState
p.paramsLen = 0
@ -276,16 +267,22 @@ func (p *Parser) performAction(action parser.Action, state parser.State, b byte)
p.clear()
case parser.PrintAction:
p.dispatch(Rune(b))
p.cmd = int(b)
if p.handler.Print != nil {
p.handler.Print(rune(b))
}
case parser.ExecuteAction:
p.dispatch(ControlCode(b))
p.cmd = int(b)
if p.handler.Execute != nil {
p.handler.Execute(b)
}
case parser.MarkerAction:
// Collect private marker
// we only store the last marker
p.cmd &^= 0xff << parser.MarkerShift
p.cmd |= int(b) << parser.MarkerShift
case parser.PrefixAction:
// Collect private prefix
// we only store the last prefix
p.cmd &^= 0xff << parser.PrefixShift
p.cmd |= int(b) << parser.PrefixShift
case parser.CollectAction:
if state == parser.Utf8State {
@ -367,11 +364,6 @@ func (p *Parser) performAction(action parser.Action, state parser.State, b byte)
p.parseStringCmd()
}
if p.dispatcher == nil {
break
}
var seq Sequence
data := p.data
if p.dataLen >= 0 {
data = data[:p.dataLen]
@ -379,23 +371,35 @@ func (p *Parser) performAction(action parser.Action, state parser.State, b byte)
switch p.state {
case parser.CsiEntryState, parser.CsiParamState, parser.CsiIntermediateState:
p.cmd |= int(b)
seq = CsiSequence{Cmd: Command(p.cmd), Params: p.Params()}
if p.handler.HandleCsi != nil {
p.handler.HandleCsi(Cmd(p.cmd), p.Params())
}
case parser.EscapeState, parser.EscapeIntermediateState:
p.cmd |= int(b)
seq = EscSequence(p.cmd)
if p.handler.HandleEsc != nil {
p.handler.HandleEsc(Cmd(p.cmd))
}
case parser.DcsEntryState, parser.DcsParamState, parser.DcsIntermediateState, parser.DcsStringState:
seq = DcsSequence{Cmd: Command(p.cmd), Params: p.Params(), Data: data}
if p.handler.HandleDcs != nil {
p.handler.HandleDcs(Cmd(p.cmd), p.Params(), data)
}
case parser.OscStringState:
seq = OscSequence{Cmd: p.cmd, Data: data}
if p.handler.HandleOsc != nil {
p.handler.HandleOsc(p.cmd, data)
}
case parser.SosStringState:
seq = SosSequence{Data: data}
if p.handler.HandleSos != nil {
p.handler.HandleSos(data)
}
case parser.PmStringState:
seq = PmSequence{Data: data}
if p.handler.HandlePm != nil {
p.handler.HandlePm(data)
}
case parser.ApcStringState:
seq = ApcSequence{Data: data}
if p.handler.HandleApc != nil {
p.handler.HandleApc(data)
}
}
p.dispatch(seq)
}
}

View File

@ -8,7 +8,7 @@ const (
NoneAction Action = iota
ClearAction
CollectAction
MarkerAction
PrefixAction
DispatchAction
ExecuteAction
StartAction // Start of a data string
@ -24,7 +24,7 @@ var ActionNames = []string{
"NoneAction",
"ClearAction",
"CollectAction",
"MarkerAction",
"PrefixAction",
"DispatchAction",
"ExecuteAction",
"StartAction",

View File

@ -4,9 +4,9 @@ import "math"
// Shift and masks for sequence parameters and intermediates.
const (
MarkerShift = 8
PrefixShift = 8
IntermedShift = 16
CommandMask = 0xff
FinalMask = 0xff
HasMoreFlag = math.MinInt32
ParamMask = ^HasMoreFlag
MissingParam = ParamMask
@ -22,12 +22,12 @@ const (
DefaultParamValue = 0
)
// Marker returns the marker byte of the sequence.
// Prefix returns the prefix byte of the sequence.
// This is always gonna be one of the following '<' '=' '>' '?' and in the
// range of 0x3C-0x3F.
// Zero is returned if the sequence does not have a marker.
func Marker(cmd int) int {
return (cmd >> MarkerShift) & CommandMask
// Zero is returned if the sequence does not have a prefix.
func Prefix(cmd int) int {
return (cmd >> PrefixShift) & FinalMask
}
// Intermediate returns the intermediate byte of the sequence.
@ -36,12 +36,12 @@ func Marker(cmd int) int {
// ',', '-', '.', '/'.
// Zero is returned if the sequence does not have an intermediate byte.
func Intermediate(cmd int) int {
return (cmd >> IntermedShift) & CommandMask
return (cmd >> IntermedShift) & FinalMask
}
// Command returns the command byte of the CSI sequence.
func Command(cmd int) int {
return cmd & CommandMask
return cmd & FinalMask
}
// Param returns the parameter at the given index.

View File

@ -178,7 +178,7 @@ func GenerateTransitionTable() TransitionTable {
table.AddRange(0x20, 0x2F, DcsEntryState, CollectAction, DcsIntermediateState)
// Dcs_entry -> Dcs_param
table.AddRange(0x30, 0x3B, DcsEntryState, ParamAction, DcsParamState)
table.AddRange(0x3C, 0x3F, DcsEntryState, MarkerAction, DcsParamState)
table.AddRange(0x3C, 0x3F, DcsEntryState, PrefixAction, DcsParamState)
// Dcs_entry -> Dcs_passthrough
table.AddRange(0x08, 0x0D, DcsEntryState, PutAction, DcsStringState) // Follows ECMA-48 § 8.3.27
// XXX: allows passing ESC (not a ECMA-48 standard) this to allow for
@ -254,7 +254,7 @@ func GenerateTransitionTable() TransitionTable {
table.AddRange(0x20, 0x2F, CsiEntryState, CollectAction, CsiIntermediateState)
// Csi_entry -> Csi_param
table.AddRange(0x30, 0x3B, CsiEntryState, ParamAction, CsiParamState)
table.AddRange(0x3C, 0x3F, CsiEntryState, MarkerAction, CsiParamState)
table.AddRange(0x3C, 0x3F, CsiEntryState, PrefixAction, CsiParamState)
// Osc_string
table.AddRange(0x00, 0x06, OscStringState, IgnoreAction, OscStringState)

View File

@ -4,6 +4,7 @@ import (
"unicode/utf8"
"github.com/charmbracelet/x/ansi/parser"
"github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
)
@ -14,7 +15,7 @@ type State = byte
// ANSI escape sequence states used by [DecodeSequence].
const (
NormalState State = iota
MarkerState
PrefixState
ParamsState
IntermedState
EscapeState
@ -33,25 +34,25 @@ const (
//
// Passing a non-nil [*Parser] as the last argument will allow the decoder to
// collect sequence parameters, data, and commands. The parser cmd will have
// the packed command value that contains intermediate and marker characters.
// the packed command value that contains intermediate and prefix characters.
// In the case of a OSC sequence, the cmd will be the OSC command number. Use
// [Command] and [Parameter] types to unpack command intermediates and markers as well
// [Cmd] and [Param] types to unpack command intermediates and prefixes as well
// as parameters.
//
// Zero [Command] means the CSI, DCS, or ESC sequence is invalid. Moreover, checking the
// Zero [Cmd] means the CSI, DCS, or ESC sequence is invalid. Moreover, checking the
// validity of other data sequences, OSC, DCS, etc, will require checking for
// the returned sequence terminator bytes such as ST (ESC \\) and BEL).
//
// We store the command byte in [Command] in the most significant byte, the
// marker byte in the next byte, and the intermediate byte in the least
// We store the command byte in [Cmd] in the most significant byte, the
// prefix byte in the next byte, and the intermediate byte in the least
// significant byte. This is done to avoid using a struct to store the command
// and its intermediates and markers. The command byte is always the least
// significant byte i.e. [Cmd & 0xff]. Use the [Command] type to unpack the
// command, intermediate, and marker bytes. Note that we only collect the last
// marker character and intermediate byte.
// and its intermediates and prefixes. The command byte is always the least
// significant byte i.e. [Cmd & 0xff]. Use the [Cmd] type to unpack the
// command, intermediate, and prefix bytes. Note that we only collect the last
// prefix character and intermediate byte.
//
// The [p.Params] slice will contain the parameters of the sequence. Any
// sub-parameter will have the [parser.HasMoreFlag] set. Use the [Parameter] type
// sub-parameter will have the [parser.HasMoreFlag] set. Use the [Param] type
// to unpack the parameters.
//
// Example:
@ -65,7 +66,63 @@ const (
// state = newState
// input = input[n:]
// }
//
// This function treats the text as a sequence of grapheme clusters.
func DecodeSequence[T string | []byte](b T, state byte, p *Parser) (seq T, width int, n int, newState byte) {
return decodeSequence(GraphemeWidth, b, state, p)
}
// DecodeSequenceWc decodes the first ANSI escape sequence or a printable
// grapheme from the given data. It returns the sequence slice, the number of
// bytes read, the cell width for each sequence, and the new state.
//
// The cell width will always be 0 for control and escape sequences, 1 for
// ASCII printable characters, and the number of cells other Unicode characters
// occupy. It uses the uniseg package to calculate the width of Unicode
// graphemes and characters. This means it will always do grapheme clustering
// (mode 2027).
//
// Passing a non-nil [*Parser] as the last argument will allow the decoder to
// collect sequence parameters, data, and commands. The parser cmd will have
// the packed command value that contains intermediate and prefix characters.
// In the case of a OSC sequence, the cmd will be the OSC command number. Use
// [Cmd] and [Param] types to unpack command intermediates and prefixes as well
// as parameters.
//
// Zero [Cmd] means the CSI, DCS, or ESC sequence is invalid. Moreover, checking the
// validity of other data sequences, OSC, DCS, etc, will require checking for
// the returned sequence terminator bytes such as ST (ESC \\) and BEL).
//
// We store the command byte in [Cmd] in the most significant byte, the
// prefix byte in the next byte, and the intermediate byte in the least
// significant byte. This is done to avoid using a struct to store the command
// and its intermediates and prefixes. The command byte is always the least
// significant byte i.e. [Cmd & 0xff]. Use the [Cmd] type to unpack the
// command, intermediate, and prefix bytes. Note that we only collect the last
// prefix character and intermediate byte.
//
// The [p.Params] slice will contain the parameters of the sequence. Any
// sub-parameter will have the [parser.HasMoreFlag] set. Use the [Param] type
// to unpack the parameters.
//
// Example:
//
// var state byte // the initial state is always zero [NormalState]
// p := NewParser(32, 1024) // create a new parser with a 32 params buffer and 1024 data buffer (optional)
// input := []byte("\x1b[31mHello, World!\x1b[0m")
// for len(input) > 0 {
// seq, width, n, newState := DecodeSequenceWc(input, state, p)
// log.Printf("seq: %q, width: %d", seq, width)
// state = newState
// input = input[n:]
// }
//
// This function treats the text as a sequence of wide characters and runes.
func DecodeSequenceWc[T string | []byte](b T, state byte, p *Parser) (seq T, width int, n int, newState byte) {
return decodeSequence(WcWidth, b, state, p)
}
func decodeSequence[T string | []byte](m Method, b T, state State, p *Parser) (seq T, width int, n int, newState byte) {
for i := 0; i < len(b); i++ {
c := b[i]
@ -92,7 +149,7 @@ func DecodeSequence[T string | []byte](b T, state byte, p *Parser) (seq T, width
p.paramsLen = 0
p.dataLen = 0
}
state = MarkerState
state = PrefixState
continue
case OSC, APC, SOS, PM:
if p != nil {
@ -120,18 +177,21 @@ func DecodeSequence[T string | []byte](b T, state byte, p *Parser) (seq T, width
if utf8.RuneStart(c) {
seq, _, width, _ = FirstGraphemeCluster(b, -1)
if m == WcWidth {
width = runewidth.StringWidth(string(seq))
}
i += len(seq)
return b[:i], width, i, NormalState
}
// Invalid UTF-8 sequence
return b[:i], 0, i, NormalState
case MarkerState:
case PrefixState:
if c >= '<' && c <= '?' {
if p != nil {
// We only collect the last marker character.
p.cmd &^= 0xff << parser.MarkerShift
p.cmd |= int(c) << parser.MarkerShift
// We only collect the last prefix character.
p.cmd &^= 0xff << parser.PrefixShift
p.cmd |= int(c) << parser.PrefixShift
}
break
}
@ -216,7 +276,7 @@ func DecodeSequence[T string | []byte](b T, state byte, p *Parser) (seq T, width
p.paramsLen = 0
p.cmd = 0
}
state = MarkerState
state = PrefixState
continue
case ']', 'X', '^', '_':
if p != nil {
@ -389,17 +449,17 @@ func FirstGraphemeCluster[T string | []byte](b T, state int) (T, T, int, int) {
panic("unreachable")
}
// Command represents a sequence command. This is used to pack/unpack a sequence
// command with its intermediate and marker characters. Those are commonly
// Cmd represents a sequence command. This is used to pack/unpack a sequence
// command with its intermediate and prefix characters. Those are commonly
// found in CSI and DCS sequences.
type Command int
type Cmd int
// Marker returns the unpacked marker byte of the CSI sequence.
// Prefix returns the unpacked prefix byte of the CSI sequence.
// This is always gonna be one of the following '<' '=' '>' '?' and in the
// range of 0x3C-0x3F.
// Zero is returned if the sequence does not have a marker.
func (c Command) Marker() int {
return parser.Marker(int(c))
// Zero is returned if the sequence does not have a prefix.
func (c Cmd) Prefix() byte {
return byte(parser.Prefix(int(c)))
}
// Intermediate returns the unpacked intermediate byte of the CSI sequence.
@ -407,37 +467,40 @@ func (c Command) Marker() int {
// characters from ' ', '!', '"', '#', '$', '%', '&', ”', '(', ')', '*', '+',
// ',', '-', '.', '/'.
// Zero is returned if the sequence does not have an intermediate byte.
func (c Command) Intermediate() int {
return parser.Intermediate(int(c))
func (c Cmd) Intermediate() byte {
return byte(parser.Intermediate(int(c)))
}
// Command returns the unpacked command byte of the CSI sequence.
func (c Command) Command() int {
return parser.Command(int(c))
// Final returns the unpacked command byte of the CSI sequence.
func (c Cmd) Final() byte {
return byte(parser.Command(int(c)))
}
// Cmd returns a packed [Command] with the given command, marker, and
// intermediate.
// The first byte is the command, the next shift is the marker, and the next
// shift is the intermediate.
// Command packs a command with the given prefix, intermediate, and final. A
// zero byte means the sequence does not have a prefix or intermediate.
//
// Even though this function takes integers, it only uses the lower 8 bits of
// each integer.
func Cmd(marker, inter, cmd int) (c Command) {
c = Command(cmd & parser.CommandMask)
c |= Command(marker&parser.CommandMask) << parser.MarkerShift
c |= Command(inter&parser.CommandMask) << parser.IntermedShift
// Prefixes are in the range of 0x3C-0x3F that is one of `<=>?`.
//
// Intermediates are in the range of 0x20-0x2F that is anything in
// `!"#$%&'()*+,-./`.
//
// Final bytes are in the range of 0x40-0x7E that is anything in the range
// `@AZ[\]^_`az{|}~`.
func Command(prefix, inter, final byte) (c int) {
c = int(final)
c |= int(prefix) << parser.PrefixShift
c |= int(inter) << parser.IntermedShift
return
}
// Parameter represents a sequence parameter. Sequence parameters with
// Param represents a sequence parameter. Sequence parameters with
// sub-parameters are packed with the HasMoreFlag set. This is used to unpack
// the parameters from a CSI and DCS sequences.
type Parameter int
type Param int
// Param returns the unpacked parameter at the given index.
// It returns the default value if the parameter is missing.
func (s Parameter) Param(def int) int {
func (s Param) Param(def int) int {
p := int(s) & parser.ParamMask
if p == parser.MissingParam {
return def
@ -446,16 +509,16 @@ func (s Parameter) Param(def int) int {
}
// HasMore unpacks the HasMoreFlag from the parameter.
func (s Parameter) HasMore() bool {
func (s Param) HasMore() bool {
return s&parser.HasMoreFlag != 0
}
// Param returns a packed [Parameter] with the given parameter and whether this
// parameter has following sub-parameters.
func Param(p int, hasMore bool) (s Parameter) {
s = Parameter(p & parser.ParamMask)
// Parameter packs an escape code parameter with the given parameter and
// whether this parameter has following sub-parameters.
func Parameter(p int, hasMore bool) (s int) {
s = p & parser.ParamMask
if hasMore {
s |= Parameter(parser.HasMoreFlag)
s |= parser.HasMoreFlag
}
return
}

View File

@ -0,0 +1,60 @@
package ansi
import "unsafe"
// Params represents a list of packed parameters.
type Params []Param
// Param returns the parameter at the given index and if it is part of a
// sub-parameters. It falls back to the default value if the parameter is
// missing. If the index is out of bounds, it returns the default value and
// false.
func (p Params) Param(i, def int) (int, bool, bool) {
if i < 0 || i >= len(p) {
return def, false, false
}
return p[i].Param(def), p[i].HasMore(), true
}
// ForEach iterates over the parameters and calls the given function for each
// parameter. If a parameter is part of a sub-parameter, it will be called with
// hasMore set to true.
// Use def to set a default value for missing parameters.
func (p Params) ForEach(def int, f func(i, param int, hasMore bool)) {
for i := range p {
f(i, p[i].Param(def), p[i].HasMore())
}
}
// ToParams converts a list of integers to a list of parameters.
func ToParams(params []int) Params {
return unsafe.Slice((*Param)(unsafe.Pointer(&params[0])), len(params))
}
// Handler handles actions performed by the parser.
// It is used to handle ANSI escape sequences, control characters, and runes.
type Handler struct {
// Print is called when a printable rune is encountered.
Print func(r rune)
// Execute is called when a control character is encountered.
Execute func(b byte)
// HandleCsi is called when a CSI sequence is encountered.
HandleCsi func(cmd Cmd, params Params)
// HandleEsc is called when an ESC sequence is encountered.
HandleEsc func(cmd Cmd)
// HandleDcs is called when a DCS sequence is encountered.
HandleDcs func(cmd Cmd, params Params, data []byte)
// HandleOsc is called when an OSC sequence is encountered.
HandleOsc func(cmd int, data []byte)
// HandlePm is called when a PM sequence is encountered.
HandlePm func(data []byte)
// HandleApc is called when an APC sequence is encountered.
HandleApc func(data []byte)
// HandleSos is called when a SOS sequence is encountered.
HandleSos func(data []byte)
}
// SetHandler sets the handler for the parser.
func (p *Parser) SetHandler(h Handler) {
p.handler = h
}

View File

@ -8,7 +8,7 @@ import (
var parserPool = sync.Pool{
New: func() any {
p := NewParser(nil)
p := NewParser()
p.SetParamsSize(parser.MaxParamsSize)
p.SetDataSize(1024 * 1024 * 4) // 4MB of data buffer
return p

View File

@ -213,6 +213,7 @@ func DECSLRM(left, right int) string {
// CSI <top> ; <bottom> r
//
// See: https://vt100.net/docs/vt510-rm/DECSTBM.html
//
// Deprecated: use [SetTopBottomMargins] instead.
func SetScrollingRegion(t, b int) string {
if t < 0 {

View File

@ -1,217 +0,0 @@
package ansi
import (
"bytes"
"github.com/charmbracelet/x/ansi/parser"
)
// Sequence represents an ANSI sequence. This can be a control sequence, escape
// sequence, a printable character, etc.
// A Sequence can be one of the following types:
// - [Rune]
// - [ControlCode]
// - [Grapheme]
// - [EscSequence]
// - [CsiSequence]
// - [OscSequence]
// - [DcsSequence]
// - [SosSequence]
// - [PmSequence]
// - [ApcSequence]
type Sequence interface {
// Clone returns a deep copy of the sequence.
Clone() Sequence
}
// Rune represents a printable character.
type Rune rune
var _ Sequence = Rune(0)
// Clone returns a deep copy of the rune.
func (r Rune) Clone() Sequence {
return r
}
// Grapheme represents a grapheme cluster.
type Grapheme struct {
Cluster string
Width int
}
var _ Sequence = Grapheme{}
// Clone returns a deep copy of the grapheme.
func (g Grapheme) Clone() Sequence {
return g
}
// ControlCode represents a control code character. This is a character that
// is not printable and is used to control the terminal. This would be a
// character in the C0 or C1 set in the range of 0x00-0x1F and 0x80-0x9F.
type ControlCode byte
var _ Sequence = ControlCode(0)
// Bytes implements Sequence.
func (c ControlCode) Bytes() []byte {
return []byte{byte(c)}
}
// String implements Sequence.
func (c ControlCode) String() string {
return string(c)
}
// Clone returns a deep copy of the control code.
func (c ControlCode) Clone() Sequence {
return c
}
// EscSequence represents an escape sequence.
type EscSequence Command
var _ Sequence = EscSequence(0)
// buffer returns the buffer of the escape sequence.
func (e EscSequence) buffer() *bytes.Buffer {
var b bytes.Buffer
b.WriteByte('\x1b')
if i := parser.Intermediate(int(e)); i != 0 {
b.WriteByte(byte(i))
}
if cmd := e.Command(); cmd != 0 {
b.WriteByte(byte(cmd))
}
return &b
}
// Bytes implements Sequence.
func (e EscSequence) Bytes() []byte {
return e.buffer().Bytes()
}
// String implements Sequence.
func (e EscSequence) String() string {
return e.buffer().String()
}
// Clone returns a deep copy of the escape sequence.
func (e EscSequence) Clone() Sequence {
return e
}
// Command returns the command byte of the escape sequence.
func (e EscSequence) Command() int {
return Command(e).Command()
}
// Intermediate returns the intermediate byte of the escape sequence.
func (e EscSequence) Intermediate() int {
return Command(e).Intermediate()
}
// SosSequence represents a SOS sequence.
type SosSequence struct {
// Data contains the raw data of the sequence.
Data []byte
}
var _ Sequence = SosSequence{}
// Bytes implements Sequence.
func (s SosSequence) Bytes() []byte {
return s.buffer().Bytes()
}
// String implements Sequence.
func (s SosSequence) String() string {
return s.buffer().String()
}
func (s SosSequence) buffer() *bytes.Buffer {
var b bytes.Buffer
b.WriteByte('\x1b')
b.WriteByte('X')
b.Write(s.Data)
b.WriteString("\x1b\\")
return &b
}
// Clone returns a deep copy of the SOS sequence.
func (s SosSequence) Clone() Sequence {
return SosSequence{
Data: append([]byte(nil), s.Data...),
}
}
// PmSequence represents a PM sequence.
type PmSequence struct {
// Data contains the raw data of the sequence.
Data []byte
}
var _ Sequence = PmSequence{}
// Bytes implements Sequence.
func (s PmSequence) Bytes() []byte {
return s.buffer().Bytes()
}
// String implements Sequence.
func (s PmSequence) String() string {
return s.buffer().String()
}
// buffer returns the buffer of the PM sequence.
func (s PmSequence) buffer() *bytes.Buffer {
var b bytes.Buffer
b.WriteByte('\x1b')
b.WriteByte('^')
b.Write(s.Data)
b.WriteString("\x1b\\")
return &b
}
// Clone returns a deep copy of the PM sequence.
func (p PmSequence) Clone() Sequence {
return PmSequence{
Data: append([]byte(nil), p.Data...),
}
}
// ApcSequence represents an APC sequence.
type ApcSequence struct {
// Data contains the raw data of the sequence.
Data []byte
}
var _ Sequence = ApcSequence{}
// Clone returns a deep copy of the APC sequence.
func (a ApcSequence) Clone() Sequence {
return ApcSequence{
Data: append([]byte(nil), a.Data...),
}
}
// Bytes implements Sequence.
func (s ApcSequence) Bytes() []byte {
return s.buffer().Bytes()
}
// String implements Sequence.
func (s ApcSequence) String() string {
return s.buffer().String()
}
// buffer returns the buffer of the APC sequence.
func (s ApcSequence) buffer() *bytes.Buffer {
var b bytes.Buffer
b.WriteByte('\x1b')
b.WriteByte('_')
b.Write(s.Data)
b.WriteString("\x1b\\")
return &b
}

View File

@ -5,25 +5,25 @@ import (
"strings"
)
// Status represents a terminal status report.
type Status interface {
// Status returns the status report identifier.
Status() int
// StatusReport represents a terminal status report.
type StatusReport interface {
// StatusReport returns the status report identifier.
StatusReport() int
}
// ANSIStatus represents an ANSI terminal status report.
type ANSIStatus int //nolint:revive
// ANSIReport represents an ANSI terminal status report.
type ANSIStatusReport int //nolint:revive
// Status returns the status report identifier.
func (s ANSIStatus) Status() int {
// Report returns the status report identifier.
func (s ANSIStatusReport) StatusReport() int {
return int(s)
}
// DECStatus represents a DEC terminal status report.
type DECStatus int
// DECStatusReport represents a DEC terminal status report.
type DECStatusReport int
// Status returns the status report identifier.
func (s DECStatus) Status() int {
func (s DECStatusReport) StatusReport() int {
return int(s)
}
@ -38,14 +38,14 @@ func (s DECStatus) Status() int {
// format.
//
// See also https://vt100.net/docs/vt510-rm/DSR.html
func DeviceStatusReport(statues ...Status) string {
func DeviceStatusReport(statues ...StatusReport) string {
var dec bool
list := make([]string, len(statues))
seq := "\x1b["
for i, status := range statues {
list[i] = strconv.Itoa(status.Status())
list[i] = strconv.Itoa(status.StatusReport())
switch status.(type) {
case DECStatus:
case DECStatusReport:
dec = true
}
}
@ -56,10 +56,39 @@ func DeviceStatusReport(statues ...Status) string {
}
// DSR is an alias for [DeviceStatusReport].
func DSR(status Status) string {
func DSR(status StatusReport) string {
return DeviceStatusReport(status)
}
// RequestCursorPositionReport is an escape sequence that requests the current
// cursor position.
//
// CSI 6 n
//
// The terminal will report the cursor position as a CSI sequence in the
// following format:
//
// CSI Pl ; Pc R
//
// Where Pl is the line number and Pc is the column number.
// See: https://vt100.net/docs/vt510-rm/CPR.html
const RequestCursorPositionReport = "\x1b[6n"
// RequestExtendedCursorPositionReport (DECXCPR) is a sequence for requesting
// the cursor position report including the current page number.
//
// CSI ? 6 n
//
// The terminal will report the cursor position as a CSI sequence in the
// following format:
//
// CSI ? Pl ; Pc ; Pp R
//
// Where Pl is the line number, Pc is the column number, and Pp is the page
// number.
// See: https://vt100.net/docs/vt510-rm/DECXCPR.html
const RequestExtendedCursorPositionReport = "\x1b[?6n"
// CursorPositionReport (CPR) is a control sequence that reports the cursor's
// position.
//

View File

@ -199,7 +199,7 @@ func (s Style) UnderlineColor(c Color) Style {
// UnderlineStyle represents an ANSI SGR (Select Graphic Rendition) underline
// style.
type UnderlineStyle = int
type UnderlineStyle = byte
const (
doubleUnderlineStyle = "4:2"
@ -487,3 +487,174 @@ func underlineColorString(c Color) string {
}
return defaultUnderlineColorAttr
}
// ReadStyleColor decodes a color from a slice of parameters. It returns the
// number of parameters read and the color. This function is used to read SGR
// color parameters following the ITU T.416 standard.
//
// It supports reading the following color types:
// - 0: implementation defined
// - 1: transparent
// - 2: RGB direct color
// - 3: CMY direct color
// - 4: CMYK direct color
// - 5: indexed color
// - 6: RGBA direct color (WezTerm extension)
//
// The parameters can be separated by semicolons (;) or colons (:). Mixing
// separators is not allowed.
//
// The specs supports defining a color space id, a color tolerance value, and a
// tolerance color space id. However, these values have no effect on the
// returned color and will be ignored.
//
// This implementation includes a few modifications to the specs:
// 1. Support for legacy color values separated by semicolons (;) with respect to RGB, and indexed colors
// 2. Support ignoring and omitting the color space id (second parameter) with respect to RGB colors
// 3. Support ignoring and omitting the 6th parameter with respect to RGB and CMY colors
// 4. Support reading RGBA colors
func ReadStyleColor(params Params, co *color.Color) (n int) {
if len(params) < 2 { // Need at least SGR type and color type
return 0
}
// First parameter indicates one of 38, 48, or 58 (foreground, background, or underline)
s := params[0]
p := params[1]
colorType := p.Param(0)
n = 2
paramsfn := func() (p1, p2, p3, p4 int) {
// Where should we start reading the color?
switch {
case s.HasMore() && p.HasMore() && len(params) > 8 && params[2].HasMore() && params[3].HasMore() && params[4].HasMore() && params[5].HasMore() && params[6].HasMore() && params[7].HasMore():
// We have color space id, a 6th parameter, a tolerance value, and a tolerance color space
n += 7
return params[3].Param(0), params[4].Param(0), params[5].Param(0), params[6].Param(0)
case s.HasMore() && p.HasMore() && len(params) > 7 && params[2].HasMore() && params[3].HasMore() && params[4].HasMore() && params[5].HasMore() && params[6].HasMore():
// We have color space id, a 6th parameter, and a tolerance value
n += 6
return params[3].Param(0), params[4].Param(0), params[5].Param(0), params[6].Param(0)
case s.HasMore() && p.HasMore() && len(params) > 6 && params[2].HasMore() && params[3].HasMore() && params[4].HasMore() && params[5].HasMore():
// We have color space id and a 6th parameter
// 48 : 4 : : 1 : 2 : 3 :4
n += 5
return params[3].Param(0), params[4].Param(0), params[5].Param(0), params[6].Param(0)
case s.HasMore() && p.HasMore() && len(params) > 5 && params[2].HasMore() && params[3].HasMore() && params[4].HasMore() && !params[5].HasMore():
// We have color space
// 48 : 3 : : 1 : 2 : 3
n += 4
return params[3].Param(0), params[4].Param(0), params[5].Param(0), -1
case s.HasMore() && p.HasMore() && p.Param(0) == 2 && params[2].HasMore() && params[3].HasMore() && !params[4].HasMore():
// We have color values separated by colons (:)
// 48 : 2 : 1 : 2 : 3
fallthrough
case !s.HasMore() && !p.HasMore() && p.Param(0) == 2 && !params[2].HasMore() && !params[3].HasMore() && !params[4].HasMore():
// Support legacy color values separated by semicolons (;)
// 48 ; 2 ; 1 ; 2 ; 3
n += 3
return params[2].Param(0), params[3].Param(0), params[4].Param(0), -1
}
// Ambiguous SGR color
return -1, -1, -1, -1
}
switch colorType {
case 0: // implementation defined
return 2
case 1: // transparent
*co = color.Transparent
return 2
case 2: // RGB direct color
if len(params) < 5 {
return 0
}
r, g, b, _ := paramsfn()
if r == -1 || g == -1 || b == -1 {
return 0
}
*co = color.RGBA{
R: uint8(r), //nolint:gosec
G: uint8(g), //nolint:gosec
B: uint8(b), //nolint:gosec
A: 0xff,
}
return
case 3: // CMY direct color
if len(params) < 5 {
return 0
}
c, m, y, _ := paramsfn()
if c == -1 || m == -1 || y == -1 {
return 0
}
*co = color.CMYK{
C: uint8(c), //nolint:gosec
M: uint8(m), //nolint:gosec
Y: uint8(y), //nolint:gosec
K: 0,
}
return
case 4: // CMYK direct color
if len(params) < 6 {
return 0
}
c, m, y, k := paramsfn()
if c == -1 || m == -1 || y == -1 || k == -1 {
return 0
}
*co = color.CMYK{
C: uint8(c), //nolint:gosec
M: uint8(m), //nolint:gosec
Y: uint8(y), //nolint:gosec
K: uint8(k), //nolint:gosec
}
return
case 5: // indexed color
if len(params) < 3 {
return 0
}
switch {
case s.HasMore() && p.HasMore() && !params[2].HasMore():
// Colon separated indexed color
// 38 : 5 : 234
case !s.HasMore() && !p.HasMore() && !params[2].HasMore():
// Legacy semicolon indexed color
// 38 ; 5 ; 234
default:
return 0
}
*co = ExtendedColor(params[2].Param(0)) //nolint:gosec
return 3
case 6: // RGBA direct color
if len(params) < 6 {
return 0
}
r, g, b, a := paramsfn()
if r == -1 || g == -1 || b == -1 || a == -1 {
return 0
}
*co = color.RGBA{
R: uint8(r), //nolint:gosec
G: uint8(g), //nolint:gosec
B: uint8(b), //nolint:gosec
A: uint8(a), //nolint:gosec
}
return
default:
return 0
}
}

View File

@ -4,14 +4,65 @@ import (
"bytes"
"github.com/charmbracelet/x/ansi/parser"
"github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
)
// Truncate truncates a string to a given length, adding a tail to the
// end if the string is longer than the given length.
// This function is aware of ANSI escape codes and will not break them, and
// accounts for wide-characters (such as East Asians and emojis).
// 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.
// This treats the text as a sequence of graphemes.
func Cut(s string, left, right int) string {
return cut(GraphemeWidth, s, left, right)
}
// 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.
// 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)
}
func cut(m Method, s string, left, right int) string {
if right <= left {
return ""
}
truncate := Truncate
truncateLeft := TruncateLeft
if m == WcWidth {
truncate = TruncateWc
truncateLeft = TruncateWc
}
if left == 0 {
return truncate(s, right, "")
}
return truncateLeft(Truncate(s, right, ""), left, "")
}
// Truncate truncates a string to a given length, adding a tail to the end if
// the string is longer than the given length. 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).
// This treats the text as a sequence of graphemes.
func Truncate(s string, length int, tail string) string {
return truncate(GraphemeWidth, s, length, tail)
}
// TruncateWc truncates a string to a given length, adding a tail to the end if
// the string is longer than the given length. 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).
// This treats the text as a sequence of wide characters and runes.
func TruncateWc(s string, length int, tail string) string {
return truncate(WcWidth, s, length, tail)
}
func truncate(m Method, s string, length int, tail string) string {
if sw := StringWidth(s); sw <= length {
return s
}
@ -33,6 +84,7 @@ func Truncate(s string, length int, tail string) string {
// Here we iterate over the bytes of the string and collect printable
// characters and runes. We also keep track of the width of the string
// in cells.
//
// Once we reach the given length, we start ignoring characters and only
// collect ANSI escape codes until we reach the end of string.
for i < len(b) {
@ -41,6 +93,9 @@ func Truncate(s string, length int, tail string) string {
// This action happens when we transition to the Utf8State.
var width int
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
if m == WcWidth {
width = runewidth.StringWidth(string(cluster))
}
// increment the index by the length of the cluster
i += len(cluster)
@ -106,13 +161,27 @@ func Truncate(s string, length int, tail string) string {
return buf.String()
}
// TruncateLeft truncates a string from the left side to a given length, adding
// a prefix to the beginning if the string is longer than the given length.
// TruncateLeft truncates a string from the left side by removing n characters,
// adding a prefix to the beginning if the string is longer than n.
// This function is aware of ANSI escape codes and will not break them, and
// accounts for wide-characters (such as East Asians and emojis).
func TruncateLeft(s string, length int, prefix string) string {
if length == 0 {
return ""
// accounts for wide-characters (such as East-Asian characters and emojis).
// This treats the text as a sequence of graphemes.
func TruncateLeft(s string, n int, prefix string) string {
return truncateLeft(GraphemeWidth, s, n, prefix)
}
// TruncateLeftWc truncates a string from the left side by removing n characters,
// adding a prefix to the beginning if the string is longer than n.
// 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).
// This treats the text as a sequence of wide characters and runes.
func TruncateLeftWc(s string, n int, prefix string) string {
return truncateLeft(WcWidth, s, n, prefix)
}
func truncateLeft(m Method, s string, n int, prefix string) string {
if n <= 0 {
return s
}
var cluster []byte
@ -133,11 +202,14 @@ func TruncateLeft(s string, length int, prefix string) string {
if state == parser.Utf8State {
var width int
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
if m == WcWidth {
width = runewidth.StringWidth(string(cluster))
}
i += len(cluster)
curWidth += width
if curWidth > length && ignoring {
if curWidth > n && ignoring {
ignoring = false
buf.WriteString(prefix)
}
@ -146,7 +218,7 @@ func TruncateLeft(s string, length int, prefix string) string {
continue
}
if curWidth > length {
if curWidth > n {
buf.Write(cluster)
}
@ -158,7 +230,7 @@ func TruncateLeft(s string, length int, prefix string) string {
case parser.PrintAction:
curWidth++
if curWidth > length && ignoring {
if curWidth > n && ignoring {
ignoring = false
buf.WriteString(prefix)
}
@ -175,7 +247,7 @@ func TruncateLeft(s string, length int, prefix string) string {
}
pstate = state
if curWidth > length && ignoring {
if curWidth > n && ignoring {
ignoring = false
buf.WriteString(prefix)
}
@ -183,3 +255,28 @@ func TruncateLeft(s string, length int, prefix string) string {
return buf.String()
}
// ByteToGraphemeRange takes start and stop byte positions and converts them to
// grapheme-aware char positions.
// You can use this with [Truncate], [TruncateLeft], and [Cut].
func ByteToGraphemeRange(str string, byteStart, byteStop int) (charStart, charStop int) {
bytePos, charPos := 0, 0
gr := uniseg.NewGraphemes(str)
for byteStart > bytePos {
if !gr.Next() {
break
}
bytePos += len(gr.Str())
charPos += max(1, gr.Width())
}
charStart = charPos
for byteStop > bytePos {
if !gr.Next() {
break
}
bytePos += len(gr.Str())
charPos += max(1, gr.Width())
}
charStop = charPos
return
}

View File

@ -90,3 +90,17 @@ 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

@ -4,6 +4,7 @@ import (
"bytes"
"github.com/charmbracelet/x/ansi/parser"
"github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
)
@ -62,7 +63,21 @@ func Strip(s string) string {
// cells that the string will occupy when printed in a terminal. ANSI escape
// codes are ignored and wide characters (such as East Asians and emojis) are
// accounted for.
// This treats the text as a sequence of grapheme clusters.
func StringWidth(s string) int {
return stringWidth(GraphemeWidth, s)
}
// StringWidthWc returns the width of a string in cells. This is the number of
// cells that the string will occupy when printed in a terminal. ANSI escape
// codes are ignored and wide characters (such as East Asians and emojis) are
// accounted for.
// This treats the text as a sequence of wide characters and runes.
func StringWidthWc(s string) int {
return stringWidth(WcWidth, s)
}
func stringWidth(m Method, s string) int {
if s == "" {
return 0
}
@ -78,6 +93,9 @@ func StringWidth(s string) int {
if state == parser.Utf8State {
var w int
cluster, _, w, _ = uniseg.FirstGraphemeClusterInString(s[i:], -1)
if m == WcWidth {
w = runewidth.StringWidth(cluster)
}
width += w
i += len(cluster) - 1
pstate = parser.GroundState

53
vendor/github.com/charmbracelet/x/ansi/winop.go generated vendored Normal file
View File

@ -0,0 +1,53 @@
package ansi
import (
"strconv"
"strings"
)
const (
// ResizeWindowWinOp is a window operation that resizes the terminal
// window.
ResizeWindowWinOp = 4
// RequestWindowSizeWinOp is a window operation that requests a report of
// the size of the terminal window in pixels. The response is in the form:
// CSI 4 ; height ; width t
RequestWindowSizeWinOp = 14
// RequestCellSizeWinOp is a window operation that requests a report of
// the size of the terminal cell size in pixels. The response is in the form:
// CSI 6 ; height ; width t
RequestCellSizeWinOp = 16
)
// WindowOp (XTWINOPS) is a sequence that manipulates the terminal window.
//
// CSI Ps ; Ps ; Ps t
//
// Ps is a semicolon-separated list of parameters.
// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-Ps;Ps;Ps-t.1EB0
func WindowOp(p int, ps ...int) string {
if p <= 0 {
return ""
}
if len(ps) == 0 {
return "\x1b[" + strconv.Itoa(p) + "t"
}
params := make([]string, 0, len(ps)+1)
params = append(params, strconv.Itoa(p))
for _, p := range ps {
if p >= 0 {
params = append(params, strconv.Itoa(p))
}
}
return "\x1b[" + strings.Join(params, ";") + "t"
}
// XTWINOPS is an alias for [WindowOp].
func XTWINOPS(p int, ps ...int) string {
return WindowOp(p, ps...)
}

View File

@ -6,6 +6,7 @@ import (
"unicode/utf8"
"github.com/charmbracelet/x/ansi/parser"
"github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
)
@ -17,7 +18,22 @@ const nbsp = 0xA0
// wide-characters in the string.
// When preserveSpace is true, spaces at the beginning of a line will be
// preserved.
// This treats the text as a sequence of graphemes.
func Hardwrap(s string, limit int, preserveSpace bool) string {
return hardwrap(GraphemeWidth, s, limit, preserveSpace)
}
// HardwrapWc wraps a string or a block of text to a given line length, breaking
// word boundaries. This will preserve ANSI escape codes and will account for
// wide-characters in the string.
// When preserveSpace is true, spaces at the beginning of a line will be
// preserved.
// This treats the text as a sequence of wide characters and runes.
func HardwrapWc(s string, limit int, preserveSpace bool) string {
return hardwrap(WcWidth, s, limit, preserveSpace)
}
func hardwrap(m Method, s string, limit int, preserveSpace bool) string {
if limit < 1 {
return s
}
@ -42,6 +58,9 @@ func Hardwrap(s string, limit int, preserveSpace bool) string {
if state == parser.Utf8State {
var width int
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
if m == WcWidth {
width = runewidth.StringWidth(string(cluster))
}
i += len(cluster)
if curWidth+width > limit {
@ -108,7 +127,27 @@ func Hardwrap(s string, limit int, preserveSpace bool) string {
// breakpoint.
//
// Note: breakpoints must be a string of 1-cell wide rune characters.
//
// This treats the text as a sequence of graphemes.
func Wordwrap(s string, limit int, breakpoints string) string {
return wordwrap(GraphemeWidth, s, limit, breakpoints)
}
// WordwrapWc wraps a string or a block of text to a given line length, not
// breaking word boundaries. This will preserve ANSI escape codes and will
// account for wide-characters in the string.
// The breakpoints string is a list of characters that are considered
// breakpoints for word wrapping. A hyphen (-) is always considered a
// breakpoint.
//
// Note: breakpoints must be a string of 1-cell wide rune characters.
//
// This treats the text as a sequence of wide characters and runes.
func WordwrapWc(s string, limit int, breakpoints string) string {
return wordwrap(WcWidth, s, limit, breakpoints)
}
func wordwrap(m Method, s string, limit int, breakpoints string) string {
if limit < 1 {
return s
}
@ -154,6 +193,9 @@ func Wordwrap(s string, limit int, breakpoints string) string {
if state == parser.Utf8State {
var width int
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
if m == WcWidth {
width = runewidth.StringWidth(string(cluster))
}
i += len(cluster)
r, _ := utf8.DecodeRune(cluster)
@ -236,7 +278,26 @@ func Wordwrap(s string, limit int, breakpoints string) string {
// (-) is always considered a breakpoint.
//
// Note: breakpoints must be a string of 1-cell wide rune characters.
//
// This treats the text as a sequence of graphemes.
func Wrap(s string, limit int, breakpoints string) string {
return wrap(GraphemeWidth, s, limit, breakpoints)
}
// WrapWc wraps a string or a block of text to a given line length, breaking word
// boundaries if necessary. This will preserve ANSI escape codes and will
// account for wide-characters in the string. The breakpoints string is a list
// of characters that are considered breakpoints for word wrapping. A hyphen
// (-) is always considered a breakpoint.
//
// Note: breakpoints must be a string of 1-cell wide rune characters.
//
// This treats the text as a sequence of wide characters and runes.
func WrapWc(s string, limit int, breakpoints string) string {
return wrap(WcWidth, s, limit, breakpoints)
}
func wrap(m Method, s string, limit int, breakpoints string) string {
if limit < 1 {
return s
}
@ -282,6 +343,9 @@ func Wrap(s string, limit int, breakpoints string) string {
if state == parser.Utf8State {
var width int
cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1)
if m == WcWidth {
width = runewidth.StringWidth(string(cluster))
}
i += len(cluster)
r, _ := utf8.DecodeRune(cluster)

View File

@ -91,6 +91,7 @@ const (
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys
//
// Deprecated: use [SetModifyOtherKeys1] or [SetModifyOtherKeys2] instead.
func ModifyOtherKeys(mode int) string {
return "\x1b[>4;" + strconv.Itoa(mode) + "m"
@ -102,6 +103,7 @@ func ModifyOtherKeys(mode int) string {
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys
//
// Deprecated: use [ResetModifyOtherKeys] instead.
const DisableModifyOtherKeys = "\x1b[>4;0m"
@ -111,6 +113,7 @@ const DisableModifyOtherKeys = "\x1b[>4;0m"
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys
//
// Deprecated: use [SetModifyOtherKeys1] instead.
const EnableModifyOtherKeys1 = "\x1b[>4;1m"
@ -120,6 +123,7 @@ const EnableModifyOtherKeys1 = "\x1b[>4;1m"
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys
//
// Deprecated: use [SetModifyOtherKeys2] instead.
const EnableModifyOtherKeys2 = "\x1b[>4;2m"
@ -129,5 +133,6 @@ const EnableModifyOtherKeys2 = "\x1b[>4;2m"
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys
//
// Deprecated: use [QueryModifyOtherKeys] instead.
const RequestModifyOtherKeys = "\x1b[?4m"

21
vendor/github.com/charmbracelet/x/cellbuf/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Charmbracelet, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

473
vendor/github.com/charmbracelet/x/cellbuf/buffer.go generated vendored Normal file
View File

@ -0,0 +1,473 @@
package cellbuf
import (
"strings"
"github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
)
// NewCell returns a new cell. This is a convenience function that initializes a
// new cell with the given content. The cell's width is determined by the
// content using [runewidth.RuneWidth].
// This will only account for the first combined rune in the content. If the
// content is empty, it will return an empty cell with a width of 0.
func NewCell(r rune, comb ...rune) (c *Cell) {
c = new(Cell)
c.Rune = r
c.Width = runewidth.RuneWidth(r)
for _, r := range comb {
if runewidth.RuneWidth(r) > 0 {
break
}
c.Comb = append(c.Comb, r)
}
c.Comb = comb
c.Width = runewidth.StringWidth(string(append([]rune{r}, comb...)))
return
}
// NewCellString returns a new cell with the given string content. This is a
// convenience function that initializes a new cell with the given content. The
// cell's width is determined by the content using [runewidth.StringWidth].
// This will only use the first combined rune in the string. If the string is
// empty, it will return an empty cell with a width of 0.
func NewCellString(s string) (c *Cell) {
c = new(Cell)
for i, r := range s {
if i == 0 {
c.Rune = r
// We only care about the first rune's width
c.Width = runewidth.RuneWidth(r)
} else {
if runewidth.RuneWidth(r) > 0 {
break
}
c.Comb = append(c.Comb, r)
}
}
return
}
// NewGraphemeCell returns a new cell. This is a convenience function that
// initializes a new cell with the given content. The cell's width is determined
// by the content using [uniseg.FirstGraphemeClusterInString].
// This is used when the content is a grapheme cluster i.e. a sequence of runes
// that form a single visual unit.
// This will only return the first grapheme cluster in the string. If the
// string is empty, it will return an empty cell with a width of 0.
func NewGraphemeCell(s string) (c *Cell) {
g, _, w, _ := uniseg.FirstGraphemeClusterInString(s, -1)
return newGraphemeCell(g, w)
}
func newGraphemeCell(s string, w int) (c *Cell) {
c = new(Cell)
c.Width = w
for i, r := range s {
if i == 0 {
c.Rune = r
} else {
c.Comb = append(c.Comb, r)
}
}
return
}
// Line represents a line in the terminal.
// A nil cell represents an blank cell, a cell with a space character and a
// width of 1.
// If a cell has no content and a width of 0, it is a placeholder for a wide
// cell.
type Line []*Cell
// Width returns the width of the line.
func (l Line) Width() int {
return len(l)
}
// Len returns the length of the line.
func (l Line) Len() int {
return len(l)
}
// String returns the string representation of the line. Any trailing spaces
// are removed.
func (l Line) String() (s string) {
for _, c := range l {
if c == nil {
s += " "
} else if c.Empty() {
continue
} else {
s += c.String()
}
}
s = strings.TrimRight(s, " ")
return
}
// At returns the cell at the given x position.
// If the cell does not exist, it returns nil.
func (l Line) At(x int) *Cell {
if x < 0 || x >= len(l) {
return nil
}
c := l[x]
if c == nil {
newCell := BlankCell
return &newCell
}
return c
}
// Set sets the cell at the given x position. If a wide cell is given, it will
// set the cell and the following cells to [EmptyCell]. It returns true if the
// cell was set.
func (l Line) Set(x int, c *Cell) bool {
return l.set(x, c, true)
}
func (l Line) set(x int, c *Cell, clone bool) bool {
width := l.Width()
if x < 0 || x >= width {
return false
}
// When a wide cell is partially overwritten, we need
// to fill the rest of the cell with space cells to
// avoid rendering issues.
prev := l.At(x)
if prev != nil && prev.Width > 1 {
// Writing to the first wide cell
for j := 0; j < prev.Width && x+j < l.Width(); j++ {
l[x+j] = prev.Clone().Blank()
}
} else if prev != nil && prev.Width == 0 {
// Writing to wide cell placeholders
for j := 1; j < maxCellWidth && x-j >= 0; j++ {
wide := l.At(x - j)
if wide != nil && wide.Width > 1 && j < wide.Width {
for k := 0; k < wide.Width; k++ {
l[x-j+k] = wide.Clone().Blank()
}
break
}
}
}
if clone && c != nil {
// Clone the cell if not nil.
c = c.Clone()
}
if c != nil && x+c.Width > width {
// If the cell is too wide, we write blanks with the same style.
for i := 0; i < c.Width && x+i < width; i++ {
l[x+i] = c.Clone().Blank()
}
} else {
l[x] = c
// Mark wide cells with an empty cell zero width
// We set the wide cell down below
if c != nil && c.Width > 1 {
for j := 1; j < c.Width && x+j < l.Width(); j++ {
var wide Cell
l[x+j] = &wide
}
}
}
return true
}
// Buffer is a 2D grid of cells representing a screen or terminal.
type Buffer struct {
// Lines holds the lines of the buffer.
Lines []Line
}
// NewBuffer creates a new buffer with the given width and height.
// This is a convenience function that initializes a new buffer and resizes it.
func NewBuffer(width int, height int) *Buffer {
b := new(Buffer)
b.Resize(width, height)
return b
}
// String returns the string representation of the buffer.
func (b *Buffer) String() (s string) {
for i, l := range b.Lines {
s += l.String()
if i < len(b.Lines)-1 {
s += "\r\n"
}
}
return
}
// Line returns a pointer to the line at the given y position.
// If the line does not exist, it returns nil.
func (b *Buffer) Line(y int) Line {
if y < 0 || y >= len(b.Lines) {
return nil
}
return b.Lines[y]
}
// Cell implements Screen.
func (b *Buffer) Cell(x int, y int) *Cell {
if y < 0 || y >= len(b.Lines) {
return nil
}
return b.Lines[y].At(x)
}
// maxCellWidth is the maximum width a terminal cell can get.
const maxCellWidth = 4
// SetCell sets the cell at the given x, y position.
func (b *Buffer) SetCell(x, y int, c *Cell) bool {
return b.setCell(x, y, c, true)
}
// setCell sets the cell at the given x, y position. This will always clone and
// allocates a new cell if c is not nil.
func (b *Buffer) setCell(x, y int, c *Cell, clone bool) bool {
if y < 0 || y >= len(b.Lines) {
return false
}
return b.Lines[y].set(x, c, clone)
}
// Height implements Screen.
func (b *Buffer) Height() int {
return len(b.Lines)
}
// Width implements Screen.
func (b *Buffer) Width() int {
if len(b.Lines) == 0 {
return 0
}
return b.Lines[0].Width()
}
// Bounds returns the bounds of the buffer.
func (b *Buffer) Bounds() Rectangle {
return Rect(0, 0, b.Width(), b.Height())
}
// Resize resizes the buffer to the given width and height.
func (b *Buffer) Resize(width int, height int) {
if width == 0 || height == 0 {
b.Lines = nil
return
}
if width > b.Width() {
line := make(Line, width-b.Width())
for i := range b.Lines {
b.Lines[i] = append(b.Lines[i], line...)
}
} else if width < b.Width() {
for i := range b.Lines {
b.Lines[i] = b.Lines[i][:width]
}
}
if height > len(b.Lines) {
for i := len(b.Lines); i < height; i++ {
b.Lines = append(b.Lines, make(Line, width))
}
} else if height < len(b.Lines) {
b.Lines = b.Lines[:height]
}
}
// FillRect fills the buffer with the given cell and rectangle.
func (b *Buffer) FillRect(c *Cell, rect Rectangle) {
cellWidth := 1
if c != nil && c.Width > 1 {
cellWidth = c.Width
}
for y := rect.Min.Y; y < rect.Max.Y; y++ {
for x := rect.Min.X; x < rect.Max.X; x += cellWidth {
b.setCell(x, y, c, false) //nolint:errcheck
}
}
}
// Fill fills the buffer with the given cell and rectangle.
func (b *Buffer) Fill(c *Cell) {
b.FillRect(c, b.Bounds())
}
// Clear clears the buffer with space cells and rectangle.
func (b *Buffer) Clear() {
b.ClearRect(b.Bounds())
}
// ClearRect clears the buffer with space cells within the specified
// rectangles. Only cells within the rectangle's bounds are affected.
func (b *Buffer) ClearRect(rect Rectangle) {
b.FillRect(nil, rect)
}
// InsertLine inserts n lines at the given line position, with the given
// optional cell, within the specified rectangles. If no rectangles are
// specified, it inserts lines in the entire buffer. Only cells within the
// rectangle's horizontal bounds are affected. Lines are pushed out of the
// rectangle bounds and lost. This follows terminal [ansi.IL] behavior.
// It returns the pushed out lines.
func (b *Buffer) InsertLine(y, n int, c *Cell) {
b.InsertLineRect(y, n, c, b.Bounds())
}
// InsertLineRect inserts new lines at the given line position, with the
// given optional cell, within the rectangle bounds. Only cells within the
// rectangle's horizontal bounds are affected. Lines are pushed out of the
// rectangle bounds and lost. This follows terminal [ansi.IL] behavior.
func (b *Buffer) InsertLineRect(y, n int, c *Cell, rect Rectangle) {
if n <= 0 || y < rect.Min.Y || y >= rect.Max.Y || y >= b.Height() {
return
}
// Limit number of lines to insert to available space
if y+n > rect.Max.Y {
n = rect.Max.Y - y
}
// Move existing lines down within the bounds
for i := rect.Max.Y - 1; i >= y+n; i-- {
for x := rect.Min.X; x < rect.Max.X; x++ {
// We don't need to clone c here because we're just moving lines down.
b.setCell(x, i, b.Lines[i-n][x], false)
}
}
// Clear the newly inserted lines within bounds
for i := y; i < y+n; i++ {
for x := rect.Min.X; x < rect.Max.X; x++ {
b.setCell(x, i, c, true)
}
}
}
// DeleteLineRect deletes lines at the given line position, with the given
// optional cell, within the rectangle bounds. Only cells within the
// rectangle's bounds are affected. Lines are shifted up within the bounds and
// new blank lines are created at the bottom. This follows terminal [ansi.DL]
// behavior.
func (b *Buffer) DeleteLineRect(y, n int, c *Cell, rect Rectangle) {
if n <= 0 || y < rect.Min.Y || y >= rect.Max.Y || y >= b.Height() {
return
}
// Limit deletion count to available space in scroll region
if n > rect.Max.Y-y {
n = rect.Max.Y - y
}
// Shift cells up within the bounds
for dst := y; dst < rect.Max.Y-n; dst++ {
src := dst + n
for x := rect.Min.X; x < rect.Max.X; x++ {
// We don't need to clone c here because we're just moving cells up.
// b.lines[dst][x] = b.lines[src][x]
b.setCell(x, dst, b.Lines[src][x], false)
}
}
// Fill the bottom n lines with blank cells
for i := rect.Max.Y - n; i < rect.Max.Y; i++ {
for x := rect.Min.X; x < rect.Max.X; x++ {
b.setCell(x, i, c, true)
}
}
}
// DeleteLine deletes n lines at the given line position, with the given
// optional cell, within the specified rectangles. If no rectangles are
// specified, it deletes lines in the entire buffer.
func (b *Buffer) DeleteLine(y, n int, c *Cell) {
b.DeleteLineRect(y, n, c, b.Bounds())
}
// InsertCell inserts new cells at the given position, with the given optional
// cell, within the specified rectangles. If no rectangles are specified, it
// inserts cells in the entire buffer. This follows terminal [ansi.ICH]
// behavior.
func (b *Buffer) InsertCell(x, y, n int, c *Cell) {
b.InsertCellRect(x, y, n, c, b.Bounds())
}
// InsertCellRect inserts new cells at the given position, with the given
// optional cell, within the rectangle bounds. Only cells within the
// rectangle's bounds are affected, following terminal [ansi.ICH] behavior.
func (b *Buffer) InsertCellRect(x, y, n int, c *Cell, rect Rectangle) {
if n <= 0 || y < rect.Min.Y || y >= rect.Max.Y || y >= b.Height() ||
x < rect.Min.X || x >= rect.Max.X || x >= b.Width() {
return
}
// Limit number of cells to insert to available space
if x+n > rect.Max.X {
n = rect.Max.X - x
}
// Move existing cells within rectangle bounds to the right
for i := rect.Max.X - 1; i >= x+n && i-n >= rect.Min.X; i-- {
// We don't need to clone c here because we're just moving cells to the
// right.
// b.lines[y][i] = b.lines[y][i-n]
b.setCell(i, y, b.Lines[y][i-n], false)
}
// Clear the newly inserted cells within rectangle bounds
for i := x; i < x+n && i < rect.Max.X; i++ {
b.setCell(i, y, c, true)
}
}
// DeleteCell deletes cells at the given position, with the given optional
// cell, within the specified rectangles. If no rectangles are specified, it
// deletes cells in the entire buffer. This follows terminal [ansi.DCH]
// behavior.
func (b *Buffer) DeleteCell(x, y, n int, c *Cell) {
b.DeleteCellRect(x, y, n, c, b.Bounds())
}
// DeleteCellRect deletes cells at the given position, with the given
// optional cell, within the rectangle bounds. Only cells within the
// rectangle's bounds are affected, following terminal [ansi.DCH] behavior.
func (b *Buffer) DeleteCellRect(x, y, n int, c *Cell, rect Rectangle) {
if n <= 0 || y < rect.Min.Y || y >= rect.Max.Y || y >= b.Height() ||
x < rect.Min.X || x >= rect.Max.X || x >= b.Width() {
return
}
// Calculate how many positions we can actually delete
remainingCells := rect.Max.X - x
if n > remainingCells {
n = remainingCells
}
// Shift the remaining cells to the left
for i := x; i < rect.Max.X-n; i++ {
if i+n < rect.Max.X {
// We don't need to clone c here because we're just moving cells to
// the left.
// b.lines[y][i] = b.lines[y][i+n]
b.setCell(i, y, b.Lines[y][i+n], false)
}
}
// Fill the vacated positions with the given cell
for i := rect.Max.X - n; i < rect.Max.X; i++ {
b.setCell(i, y, c, true)
}
}

508
vendor/github.com/charmbracelet/x/cellbuf/cell.go generated vendored Normal file
View File

@ -0,0 +1,508 @@
package cellbuf
import (
"github.com/charmbracelet/x/ansi"
)
var (
// BlankCell is a cell with a single space, width of 1, and no style or link.
BlankCell = Cell{Rune: ' ', Width: 1}
// EmptyCell is just an empty cell used for comparisons and as a placeholder
// for wide cells.
EmptyCell = Cell{}
)
// Cell represents a single cell in the terminal screen.
type Cell struct {
// The style of the cell. Nil style means no style. Zero value prints a
// reset sequence.
Style Style
// Link is the hyperlink of the cell.
Link Link
// Comb is the combining runes of the cell. This is nil if the cell is a
// single rune or if it's a zero width cell that is part of a wider cell.
Comb []rune
// Width is the mono-space width of the grapheme cluster.
Width int
// Rune is the main rune of the cell. This is zero if the cell is part of a
// wider cell.
Rune rune
}
// Append appends runes to the cell without changing the width. This is useful
// when we want to use the cell to store escape sequences or other runes that
// don't affect the width of the cell.
func (c *Cell) Append(r ...rune) {
for i, r := range r {
if i == 0 && c.Rune == 0 {
c.Rune = r
continue
}
c.Comb = append(c.Comb, r)
}
}
// String returns the string content of the cell excluding any styles, links,
// and escape sequences.
func (c Cell) String() string {
if c.Rune == 0 {
return ""
}
if len(c.Comb) == 0 {
return string(c.Rune)
}
return string(append([]rune{c.Rune}, c.Comb...))
}
// Equal returns whether the cell is equal to the other cell.
func (c *Cell) Equal(o *Cell) bool {
return o != nil &&
c.Width == o.Width &&
c.Rune == o.Rune &&
runesEqual(c.Comb, o.Comb) &&
c.Style.Equal(&o.Style) &&
c.Link.Equal(&o.Link)
}
// Empty returns whether the cell is an empty cell. An empty cell is a cell
// with a width of 0, a rune of 0, and no combining runes.
func (c Cell) Empty() bool {
return c.Width == 0 &&
c.Rune == 0 &&
len(c.Comb) == 0
}
// Reset resets the cell to the default state zero value.
func (c *Cell) Reset() {
c.Rune = 0
c.Comb = nil
c.Width = 0
c.Style.Reset()
c.Link.Reset()
}
// Clear returns whether the cell consists of only attributes that don't
// affect appearance of a space character.
func (c *Cell) Clear() bool {
return c.Rune == ' ' && len(c.Comb) == 0 && c.Width == 1 && c.Style.Clear() && c.Link.Empty()
}
// Clone returns a copy of the cell.
func (c *Cell) Clone() (n *Cell) {
n = new(Cell)
*n = *c
return
}
// Blank makes the cell a blank cell by setting the rune to a space, comb to
// nil, and the width to 1.
func (c *Cell) Blank() *Cell {
c.Rune = ' '
c.Comb = nil
c.Width = 1
return c
}
// Link represents a hyperlink in the terminal screen.
type Link struct {
URL string
Params string
}
// String returns a string representation of the hyperlink.
func (h Link) String() string {
return h.URL
}
// Reset resets the hyperlink to the default state zero value.
func (h *Link) Reset() {
h.URL = ""
h.Params = ""
}
// Equal returns whether the hyperlink is equal to the other hyperlink.
func (h *Link) Equal(o *Link) bool {
return o != nil && h.URL == o.URL && h.Params == o.Params
}
// Empty returns whether the hyperlink is empty.
func (h Link) Empty() bool {
return h.URL == "" && h.Params == ""
}
// AttrMask is a bitmask for text attributes that can change the look of text.
// These attributes can be combined to create different styles.
type AttrMask uint8
// These are the available text attributes that can be combined to create
// different styles.
const (
BoldAttr AttrMask = 1 << iota
FaintAttr
ItalicAttr
SlowBlinkAttr
RapidBlinkAttr
ReverseAttr
ConcealAttr
StrikethroughAttr
ResetAttr AttrMask = 0
)
// Contains returns whether the attribute mask contains the attribute.
func (a AttrMask) Contains(attr AttrMask) bool {
return a&attr == attr
}
// UnderlineStyle is the style of underline to use for text.
type UnderlineStyle = ansi.UnderlineStyle
// These are the available underline styles.
const (
NoUnderline = ansi.NoUnderlineStyle
SingleUnderline = ansi.SingleUnderlineStyle
DoubleUnderline = ansi.DoubleUnderlineStyle
CurlyUnderline = ansi.CurlyUnderlineStyle
DottedUnderline = ansi.DottedUnderlineStyle
DashedUnderline = ansi.DashedUnderlineStyle
)
// Style represents the Style of a cell.
type Style struct {
Fg ansi.Color
Bg ansi.Color
Ul ansi.Color
Attrs AttrMask
UlStyle UnderlineStyle
}
// Sequence returns the ANSI sequence that sets the style.
func (s Style) Sequence() string {
if s.Empty() {
return ansi.ResetStyle
}
var b ansi.Style
if s.Attrs != 0 {
if s.Attrs&BoldAttr != 0 {
b = b.Bold()
}
if s.Attrs&FaintAttr != 0 {
b = b.Faint()
}
if s.Attrs&ItalicAttr != 0 {
b = b.Italic()
}
if s.Attrs&SlowBlinkAttr != 0 {
b = b.SlowBlink()
}
if s.Attrs&RapidBlinkAttr != 0 {
b = b.RapidBlink()
}
if s.Attrs&ReverseAttr != 0 {
b = b.Reverse()
}
if s.Attrs&ConcealAttr != 0 {
b = b.Conceal()
}
if s.Attrs&StrikethroughAttr != 0 {
b = b.Strikethrough()
}
}
if s.UlStyle != NoUnderline {
switch s.UlStyle {
case SingleUnderline:
b = b.Underline()
case DoubleUnderline:
b = b.DoubleUnderline()
case CurlyUnderline:
b = b.CurlyUnderline()
case DottedUnderline:
b = b.DottedUnderline()
case DashedUnderline:
b = b.DashedUnderline()
}
}
if s.Fg != nil {
b = b.ForegroundColor(s.Fg)
}
if s.Bg != nil {
b = b.BackgroundColor(s.Bg)
}
if s.Ul != nil {
b = b.UnderlineColor(s.Ul)
}
return b.String()
}
// DiffSequence returns the ANSI sequence that sets the style as a diff from
// another style.
func (s Style) DiffSequence(o Style) string {
if o.Empty() {
return s.Sequence()
}
var b ansi.Style
if !colorEqual(s.Fg, o.Fg) {
b = b.ForegroundColor(s.Fg)
}
if !colorEqual(s.Bg, o.Bg) {
b = b.BackgroundColor(s.Bg)
}
if !colorEqual(s.Ul, o.Ul) {
b = b.UnderlineColor(s.Ul)
}
var (
noBlink bool
isNormal bool
)
if s.Attrs != o.Attrs {
if s.Attrs&BoldAttr != o.Attrs&BoldAttr {
if s.Attrs&BoldAttr != 0 {
b = b.Bold()
} else if !isNormal {
isNormal = true
b = b.NormalIntensity()
}
}
if s.Attrs&FaintAttr != o.Attrs&FaintAttr {
if s.Attrs&FaintAttr != 0 {
b = b.Faint()
} else if !isNormal {
b = b.NormalIntensity()
}
}
if s.Attrs&ItalicAttr != o.Attrs&ItalicAttr {
if s.Attrs&ItalicAttr != 0 {
b = b.Italic()
} else {
b = b.NoItalic()
}
}
if s.Attrs&SlowBlinkAttr != o.Attrs&SlowBlinkAttr {
if s.Attrs&SlowBlinkAttr != 0 {
b = b.SlowBlink()
} else if !noBlink {
noBlink = true
b = b.NoBlink()
}
}
if s.Attrs&RapidBlinkAttr != o.Attrs&RapidBlinkAttr {
if s.Attrs&RapidBlinkAttr != 0 {
b = b.RapidBlink()
} else if !noBlink {
b = b.NoBlink()
}
}
if s.Attrs&ReverseAttr != o.Attrs&ReverseAttr {
if s.Attrs&ReverseAttr != 0 {
b = b.Reverse()
} else {
b = b.NoReverse()
}
}
if s.Attrs&ConcealAttr != o.Attrs&ConcealAttr {
if s.Attrs&ConcealAttr != 0 {
b = b.Conceal()
} else {
b = b.NoConceal()
}
}
if s.Attrs&StrikethroughAttr != o.Attrs&StrikethroughAttr {
if s.Attrs&StrikethroughAttr != 0 {
b = b.Strikethrough()
} else {
b = b.NoStrikethrough()
}
}
}
if s.UlStyle != o.UlStyle {
b = b.UnderlineStyle(s.UlStyle)
}
return b.String()
}
// Equal returns true if the style is equal to the other style.
func (s *Style) Equal(o *Style) bool {
return s.Attrs == o.Attrs &&
s.UlStyle == o.UlStyle &&
colorEqual(s.Fg, o.Fg) &&
colorEqual(s.Bg, o.Bg) &&
colorEqual(s.Ul, o.Ul)
}
func colorEqual(c, o ansi.Color) bool {
if c == nil && o == nil {
return true
}
if c == nil || o == nil {
return false
}
cr, cg, cb, ca := c.RGBA()
or, og, ob, oa := o.RGBA()
return cr == or && cg == og && cb == ob && ca == oa
}
// Bold sets the bold attribute.
func (s *Style) Bold(v bool) *Style {
if v {
s.Attrs |= BoldAttr
} else {
s.Attrs &^= BoldAttr
}
return s
}
// Faint sets the faint attribute.
func (s *Style) Faint(v bool) *Style {
if v {
s.Attrs |= FaintAttr
} else {
s.Attrs &^= FaintAttr
}
return s
}
// Italic sets the italic attribute.
func (s *Style) Italic(v bool) *Style {
if v {
s.Attrs |= ItalicAttr
} else {
s.Attrs &^= ItalicAttr
}
return s
}
// SlowBlink sets the slow blink attribute.
func (s *Style) SlowBlink(v bool) *Style {
if v {
s.Attrs |= SlowBlinkAttr
} else {
s.Attrs &^= SlowBlinkAttr
}
return s
}
// RapidBlink sets the rapid blink attribute.
func (s *Style) RapidBlink(v bool) *Style {
if v {
s.Attrs |= RapidBlinkAttr
} else {
s.Attrs &^= RapidBlinkAttr
}
return s
}
// Reverse sets the reverse attribute.
func (s *Style) Reverse(v bool) *Style {
if v {
s.Attrs |= ReverseAttr
} else {
s.Attrs &^= ReverseAttr
}
return s
}
// Conceal sets the conceal attribute.
func (s *Style) Conceal(v bool) *Style {
if v {
s.Attrs |= ConcealAttr
} else {
s.Attrs &^= ConcealAttr
}
return s
}
// Strikethrough sets the strikethrough attribute.
func (s *Style) Strikethrough(v bool) *Style {
if v {
s.Attrs |= StrikethroughAttr
} else {
s.Attrs &^= StrikethroughAttr
}
return s
}
// UnderlineStyle sets the underline style.
func (s *Style) UnderlineStyle(style UnderlineStyle) *Style {
s.UlStyle = style
return s
}
// Underline sets the underline attribute.
// This is a syntactic sugar for [UnderlineStyle].
func (s *Style) Underline(v bool) *Style {
if v {
return s.UnderlineStyle(SingleUnderline)
}
return s.UnderlineStyle(NoUnderline)
}
// Foreground sets the foreground color.
func (s *Style) Foreground(c ansi.Color) *Style {
s.Fg = c
return s
}
// Background sets the background color.
func (s *Style) Background(c ansi.Color) *Style {
s.Bg = c
return s
}
// UnderlineColor sets the underline color.
func (s *Style) UnderlineColor(c ansi.Color) *Style {
s.Ul = c
return s
}
// Reset resets the style to default.
func (s *Style) Reset() *Style {
s.Fg = nil
s.Bg = nil
s.Ul = nil
s.Attrs = ResetAttr
s.UlStyle = NoUnderline
return s
}
// Empty returns true if the style is empty.
func (s *Style) Empty() bool {
return s.Fg == nil && s.Bg == nil && s.Ul == nil && s.Attrs == ResetAttr && s.UlStyle == NoUnderline
}
// Clear returns whether the style consists of only attributes that don't
// affect appearance of a space character.
func (s *Style) Clear() bool {
return s.UlStyle == NoUnderline &&
s.Attrs&^(BoldAttr|FaintAttr|ItalicAttr|SlowBlinkAttr|RapidBlinkAttr) == 0 &&
s.Fg == nil &&
s.Bg == nil &&
s.Ul == nil
}
func runesEqual(a, b []rune) bool {
if len(a) != len(b) {
return false
}
for i, r := range a {
if r != b[i] {
return false
}
}
return true
}

6
vendor/github.com/charmbracelet/x/cellbuf/errors.go generated vendored Normal file
View File

@ -0,0 +1,6 @@
package cellbuf
import "errors"
// ErrOutOfBounds is returned when the given x, y position is out of bounds.
var ErrOutOfBounds = errors.New("out of bounds")

21
vendor/github.com/charmbracelet/x/cellbuf/geom.go generated vendored Normal file
View File

@ -0,0 +1,21 @@
package cellbuf
import (
"image"
)
// Position represents an x, y position.
type Position = image.Point
// Pos is a shorthand for Position{X: x, Y: y}.
func Pos(x, y int) Position {
return image.Pt(x, y)
}
// Rectange represents a rectangle.
type Rectangle = image.Rectangle
// Rect is a shorthand for Rectangle.
func Rect(x, y, w, h int) Rectangle {
return image.Rect(x, y, x+w, y+h)
}

272
vendor/github.com/charmbracelet/x/cellbuf/hardscroll.go generated vendored Normal file
View File

@ -0,0 +1,272 @@
package cellbuf
import (
"strings"
"github.com/charmbracelet/x/ansi"
)
// scrollOptimize optimizes the screen to transform the old buffer into the new
// buffer.
func (s *Screen) scrollOptimize() {
height := s.newbuf.Height()
if s.oldnum == nil || len(s.oldnum) < height {
s.oldnum = make([]int, height)
}
// Calculate the indices
s.updateHashmap()
if len(s.hashtab) < height {
return
}
// Pass 1 - from top to bottom scrolling up
for i := 0; i < height; {
for i < height && (s.oldnum[i] == newIndex || s.oldnum[i] <= i) {
i++
}
if i >= height {
break
}
shift := s.oldnum[i] - i // shift > 0
start := i
i++
for i < height && s.oldnum[i] != newIndex && s.oldnum[i]-i == shift {
i++
}
end := i - 1 + shift
if !s.scrolln(shift, start, end, height-1) {
continue
}
}
// Pass 2 - from bottom to top scrolling down
for i := height - 1; i >= 0; {
for i >= 0 && (s.oldnum[i] == newIndex || s.oldnum[i] >= i) {
i--
}
if i < 0 {
break
}
shift := s.oldnum[i] - i // shift < 0
end := i
i--
for i >= 0 && s.oldnum[i] != newIndex && s.oldnum[i]-i == shift {
i--
}
start := i + 1 - (-shift)
if !s.scrolln(shift, start, end, height-1) {
continue
}
}
}
// scrolln scrolls the screen up by n lines.
func (s *Screen) scrolln(n, top, bot, maxY int) (v bool) { //nolint:unparam
const (
nonDestScrollRegion = false
memoryBelow = false
)
blank := s.clearBlank()
if n > 0 {
// Scroll up (forward)
v = s.scrollUp(n, top, bot, 0, maxY, blank)
if !v {
s.buf.WriteString(ansi.SetTopBottomMargins(top+1, bot+1))
// XXX: How should we handle this in inline mode when not using alternate screen?
s.cur.X, s.cur.Y = -1, -1
v = s.scrollUp(n, top, bot, top, bot, blank)
s.buf.WriteString(ansi.SetTopBottomMargins(1, maxY+1))
s.cur.X, s.cur.Y = -1, -1
}
if !v {
v = s.scrollIdl(n, top, bot-n+1, blank)
}
// Clear newly shifted-in lines.
if v &&
(nonDestScrollRegion || (memoryBelow && bot == maxY)) {
if bot == maxY {
s.move(0, bot-n+1)
s.clearToBottom(nil)
} else {
for i := 0; i < n; i++ {
s.move(0, bot-i)
s.clearToEnd(nil, false)
}
}
}
} else if n < 0 {
// Scroll down (backward)
v = s.scrollDown(-n, top, bot, 0, maxY, blank)
if !v {
s.buf.WriteString(ansi.SetTopBottomMargins(top+1, bot+1))
// XXX: How should we handle this in inline mode when not using alternate screen?
s.cur.X, s.cur.Y = -1, -1
v = s.scrollDown(-n, top, bot, top, bot, blank)
s.buf.WriteString(ansi.SetTopBottomMargins(1, maxY+1))
s.cur.X, s.cur.Y = -1, -1
if !v {
v = s.scrollIdl(-n, bot+n+1, top, blank)
}
// Clear newly shifted-in lines.
if v &&
(nonDestScrollRegion || (memoryBelow && top == 0)) {
for i := 0; i < -n; i++ {
s.move(0, top+i)
s.clearToEnd(nil, false)
}
}
}
}
if !v {
return
}
s.scrollBuffer(s.curbuf, n, top, bot, blank)
// shift hash values too, they can be reused
s.scrollOldhash(n, top, bot)
return true
}
// scrollBuffer scrolls the buffer by n lines.
func (s *Screen) scrollBuffer(b *Buffer, n, top, bot int, blank *Cell) {
if top < 0 || bot < top || bot >= b.Height() {
// Nothing to scroll
return
}
if n < 0 {
// shift n lines downwards
limit := top - n
for line := bot; line >= limit && line >= 0 && line >= top; line-- {
copy(b.Lines[line], b.Lines[line+n])
}
for line := top; line < limit && line <= b.Height()-1 && line <= bot; line++ {
b.FillRect(blank, Rect(0, line, b.Width(), 1))
}
}
if n > 0 {
// shift n lines upwards
limit := bot - n
for line := top; line <= limit && line <= b.Height()-1 && line <= bot; line++ {
copy(b.Lines[line], b.Lines[line+n])
}
for line := bot; line > limit && line >= 0 && line >= top; line-- {
b.FillRect(blank, Rect(0, line, b.Width(), 1))
}
}
s.touchLine(b.Width(), b.Height(), top, bot-top+1, true)
}
// touchLine marks the line as touched.
func (s *Screen) touchLine(width, height, y, n int, changed bool) {
if n < 0 || y < 0 || y >= height {
return // Nothing to touch
}
for i := y; i < y+n && i < height; i++ {
if changed {
s.touch[i] = lineData{firstCell: 0, lastCell: width - 1}
} else {
delete(s.touch, i)
}
}
}
// scrollUp scrolls the screen up by n lines.
func (s *Screen) scrollUp(n, top, bot, minY, maxY int, blank *Cell) bool {
if n == 1 && top == minY && bot == maxY {
s.move(0, bot)
s.updatePen(blank)
s.buf.WriteByte('\n')
} else if n == 1 && bot == maxY {
s.move(0, top)
s.updatePen(blank)
s.buf.WriteString(ansi.DeleteLine(1))
} else if top == minY && bot == maxY {
if s.xtermLike {
s.move(0, bot)
} else {
s.move(0, top)
}
s.updatePen(blank)
if s.xtermLike {
s.buf.WriteString(ansi.ScrollUp(n))
} else {
s.buf.WriteString(strings.Repeat("\n", n))
}
} else if bot == maxY {
s.move(0, top)
s.updatePen(blank)
s.buf.WriteString(ansi.DeleteLine(n))
} else {
return false
}
return true
}
// scrollDown scrolls the screen down by n lines.
func (s *Screen) scrollDown(n, top, bot, minY, maxY int, blank *Cell) bool {
if n == 1 && top == minY && bot == maxY {
s.move(0, top)
s.updatePen(blank)
s.buf.WriteString(ansi.ReverseIndex)
} else if n == 1 && bot == maxY {
s.move(0, top)
s.updatePen(blank)
s.buf.WriteString(ansi.InsertLine(1))
} else if top == minY && bot == maxY {
s.move(0, top)
s.updatePen(blank)
if s.xtermLike {
s.buf.WriteString(ansi.ScrollDown(n))
} else {
s.buf.WriteString(strings.Repeat(ansi.ReverseIndex, n))
}
} else if bot == maxY {
s.move(0, top)
s.updatePen(blank)
s.buf.WriteString(ansi.InsertLine(n))
} else {
return false
}
return true
}
// scrollIdl scrolls the screen n lines by using [ansi.DL] at del and using
// [ansi.IL] at ins.
func (s *Screen) scrollIdl(n, del, ins int, blank *Cell) bool {
if n < 0 {
return false
}
// Delete lines
s.move(0, del)
s.updatePen(blank)
s.buf.WriteString(ansi.DeleteLine(n))
// Insert lines
s.move(0, ins)
s.updatePen(blank)
s.buf.WriteString(ansi.InsertLine(n))
return true
}

301
vendor/github.com/charmbracelet/x/cellbuf/hashmap.go generated vendored Normal file
View File

@ -0,0 +1,301 @@
package cellbuf
import (
"github.com/charmbracelet/x/ansi"
)
// hash returns the hash value of a [Line].
func hash(l Line) (h uint64) {
for _, c := range l {
var r rune
if c == nil {
r = ansi.SP
} else {
r = c.Rune
}
h += (h << 5) + uint64(r)
}
return
}
// hashmap represents a single [Line] hash.
type hashmap struct {
value uint64
oldcount, newcount int
oldindex, newindex int
}
// The value used to indicate lines created by insertions and scrolls.
const newIndex = -1
// updateHashmap updates the hashmap with the new hash value.
func (s *Screen) updateHashmap() {
height := s.newbuf.Height()
if len(s.oldhash) >= height && len(s.newhash) >= height {
// rehash changed lines
for i := 0; i < height; i++ {
_, ok := s.touch[i]
if ok {
s.oldhash[i] = hash(s.curbuf.Line(i))
s.newhash[i] = hash(s.newbuf.Line(i))
}
}
} else {
// rehash all
if len(s.oldhash) != height {
s.oldhash = make([]uint64, height)
}
if len(s.newhash) != height {
s.newhash = make([]uint64, height)
}
for i := 0; i < height; i++ {
s.oldhash[i] = hash(s.curbuf.Line(i))
s.newhash[i] = hash(s.newbuf.Line(i))
}
}
s.hashtab = make([]hashmap, height*2)
for i := 0; i < height; i++ {
hashval := s.oldhash[i]
// Find matching hash or empty slot
idx := 0
for idx < len(s.hashtab) && s.hashtab[idx].value != 0 {
if s.hashtab[idx].value == hashval {
break
}
idx++
}
s.hashtab[idx].value = hashval // in case this is a new hash
s.hashtab[idx].oldcount++
s.hashtab[idx].oldindex = i
}
for i := 0; i < height; i++ {
hashval := s.newhash[i]
// Find matching hash or empty slot
idx := 0
for idx < len(s.hashtab) && s.hashtab[idx].value != 0 {
if s.hashtab[idx].value == hashval {
break
}
idx++
}
s.hashtab[idx].value = hashval // in case this is a new hash
s.hashtab[idx].newcount++
s.hashtab[idx].newindex = i
s.oldnum[i] = newIndex // init old indices slice
}
// Mark line pair corresponding to unique hash pairs.
for i := 0; i < len(s.hashtab) && s.hashtab[i].value != 0; i++ {
hsp := &s.hashtab[i]
if hsp.oldcount == 1 && hsp.newcount == 1 && hsp.oldindex != hsp.newindex {
s.oldnum[hsp.newindex] = hsp.oldindex
}
}
s.growHunks()
// Eliminate bad or impossible shifts. This includes removing those hunks
// which could not grow because of conflicts, as well those which are to be
// moved too far, they are likely to destroy more than carry.
for i := 0; i < height; {
var start, shift, size int
for i < height && s.oldnum[i] == newIndex {
i++
}
if i >= height {
break
}
start = i
shift = s.oldnum[i] - i
i++
for i < height && s.oldnum[i] != newIndex && s.oldnum[i]-i == shift {
i++
}
size = i - start
if size < 3 || size+min(size/8, 2) < abs(shift) {
for start < i {
s.oldnum[start] = newIndex
start++
}
}
}
// After clearing invalid hunks, try grow the rest.
s.growHunks()
}
// scrollOldhash
func (s *Screen) scrollOldhash(n, top, bot int) {
if len(s.oldhash) == 0 {
return
}
size := bot - top + 1 - abs(n)
if n > 0 {
// Move existing hashes up
copy(s.oldhash[top:], s.oldhash[top+n:top+n+size])
// Recalculate hashes for newly shifted-in lines
for i := bot; i > bot-n; i-- {
s.oldhash[i] = hash(s.curbuf.Line(i))
}
} else {
// Move existing hashes down
copy(s.oldhash[top-n:], s.oldhash[top:top+size])
// Recalculate hashes for newly shifted-in lines
for i := top; i < top-n; i++ {
s.oldhash[i] = hash(s.curbuf.Line(i))
}
}
}
func (s *Screen) growHunks() {
var (
backLimit int // limits for cells to fill
backRefLimit int // limit for references
i int
nextHunk int
)
height := s.newbuf.Height()
for i < height && s.oldnum[i] == newIndex {
i++
}
for ; i < height; i = nextHunk {
var (
forwardLimit int
forwardRefLimit int
end int
start = i
shift = s.oldnum[i] - i
)
// get forward limit
i = start + 1
for i < height &&
s.oldnum[i] != newIndex &&
s.oldnum[i]-i == shift {
i++
}
end = i
for i < height && s.oldnum[i] == newIndex {
i++
}
nextHunk = i
forwardLimit = i
if i >= height || s.oldnum[i] >= i {
forwardRefLimit = i
} else {
forwardRefLimit = s.oldnum[i]
}
i = start - 1
// grow back
if shift < 0 {
backLimit = backRefLimit + (-shift)
}
for i >= backLimit {
if s.newhash[i] == s.oldhash[i+shift] ||
s.costEffective(i+shift, i, shift < 0) {
s.oldnum[i] = i + shift
} else {
break
}
i--
}
i = end
// grow forward
if shift > 0 {
forwardLimit = forwardRefLimit - shift
}
for i < forwardLimit {
if s.newhash[i] == s.oldhash[i+shift] ||
s.costEffective(i+shift, i, shift > 0) {
s.oldnum[i] = i + shift
} else {
break
}
i++
}
backLimit = i
backRefLimit = backLimit
if shift > 0 {
backRefLimit += shift
}
}
}
// costEffective returns true if the cost of moving line 'from' to line 'to' seems to be
// cost effective. 'blank' indicates whether the line 'to' would become blank.
func (s *Screen) costEffective(from, to int, blank bool) bool {
if from == to {
return false
}
newFrom := s.oldnum[from]
if newFrom == newIndex {
newFrom = from
}
// On the left side of >= is the cost before moving. On the right side --
// cost after moving.
// Calculate costs before moving.
var costBeforeMove int
if blank {
// Cost of updating blank line at destination.
costBeforeMove = s.updateCostBlank(s.newbuf.Line(to))
} else {
// Cost of updating exiting line at destination.
costBeforeMove = s.updateCost(s.curbuf.Line(to), s.newbuf.Line(to))
}
// Add cost of updating source line
costBeforeMove += s.updateCost(s.curbuf.Line(newFrom), s.newbuf.Line(from))
// Calculate costs after moving.
var costAfterMove int
if newFrom == from {
// Source becomes blank after move
costAfterMove = s.updateCostBlank(s.newbuf.Line(from))
} else {
// Source gets updated from another line
costAfterMove = s.updateCost(s.curbuf.Line(newFrom), s.newbuf.Line(from))
}
// Add cost of moving source line to destination
costAfterMove += s.updateCost(s.curbuf.Line(from), s.newbuf.Line(to))
// Return true if moving is cost effective (costs less or equal)
return costBeforeMove >= costAfterMove
}
func (s *Screen) updateCost(from, to Line) (cost int) {
var fidx, tidx int
for i := s.newbuf.Width() - 1; i > 0; i, fidx, tidx = i-1, fidx+1, tidx+1 {
if !cellEqual(from.At(fidx), to.At(tidx)) {
cost++
}
}
return
}
func (s *Screen) updateCostBlank(to Line) (cost int) {
var tidx int
for i := s.newbuf.Width() - 1; i > 0; i, tidx = i-1, tidx+1 {
if !cellEqual(nil, to.At(tidx)) {
cost++
}
}
return
}

14
vendor/github.com/charmbracelet/x/cellbuf/link.go generated vendored Normal file
View File

@ -0,0 +1,14 @@
package cellbuf
import (
"github.com/charmbracelet/colorprofile"
)
// Convert converts a hyperlink to respect the given color profile.
func ConvertLink(h Link, p colorprofile.Profile) Link {
if p == colorprofile.NoTTY {
return Link{}
}
return h
}

1457
vendor/github.com/charmbracelet/x/cellbuf/screen.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

131
vendor/github.com/charmbracelet/x/cellbuf/sequence.go generated vendored Normal file
View File

@ -0,0 +1,131 @@
package cellbuf
import (
"bytes"
"image/color"
"github.com/charmbracelet/x/ansi"
)
// ReadStyle reads a Select Graphic Rendition (SGR) escape sequences from a
// list of parameters.
func ReadStyle(params ansi.Params, pen *Style) {
if len(params) == 0 {
pen.Reset()
return
}
for i := 0; i < len(params); i++ {
param, hasMore, _ := params.Param(i, 0)
switch param {
case 0: // Reset
pen.Reset()
case 1: // Bold
pen.Bold(true)
case 2: // Dim/Faint
pen.Faint(true)
case 3: // Italic
pen.Italic(true)
case 4: // Underline
nextParam, _, ok := params.Param(i+1, 0)
if hasMore && ok { // Only accept subparameters i.e. separated by ":"
switch nextParam {
case 0, 1, 2, 3, 4, 5:
i++
switch nextParam {
case 0: // No Underline
pen.UnderlineStyle(NoUnderline)
case 1: // Single Underline
pen.UnderlineStyle(SingleUnderline)
case 2: // Double Underline
pen.UnderlineStyle(DoubleUnderline)
case 3: // Curly Underline
pen.UnderlineStyle(CurlyUnderline)
case 4: // Dotted Underline
pen.UnderlineStyle(DottedUnderline)
case 5: // Dashed Underline
pen.UnderlineStyle(DashedUnderline)
}
}
} else {
// Single Underline
pen.Underline(true)
}
case 5: // Slow Blink
pen.SlowBlink(true)
case 6: // Rapid Blink
pen.RapidBlink(true)
case 7: // Reverse
pen.Reverse(true)
case 8: // Conceal
pen.Conceal(true)
case 9: // Crossed-out/Strikethrough
pen.Strikethrough(true)
case 22: // Normal Intensity (not bold or faint)
pen.Bold(false).Faint(false)
case 23: // Not italic, not Fraktur
pen.Italic(false)
case 24: // Not underlined
pen.Underline(false)
case 25: // Blink off
pen.SlowBlink(false).RapidBlink(false)
case 27: // Positive (not reverse)
pen.Reverse(false)
case 28: // Reveal
pen.Conceal(false)
case 29: // Not crossed out
pen.Strikethrough(false)
case 30, 31, 32, 33, 34, 35, 36, 37: // Set foreground
pen.Foreground(ansi.Black + ansi.BasicColor(param-30)) //nolint:gosec
case 38: // Set foreground 256 or truecolor
var c color.Color
n := ReadStyleColor(params[i:], &c)
if n > 0 {
pen.Foreground(c)
i += n - 1
}
case 39: // Default foreground
pen.Foreground(nil)
case 40, 41, 42, 43, 44, 45, 46, 47: // Set background
pen.Background(ansi.Black + ansi.BasicColor(param-40)) //nolint:gosec
case 48: // Set background 256 or truecolor
var c color.Color
n := ReadStyleColor(params[i:], &c)
if n > 0 {
pen.Background(c)
i += n - 1
}
case 49: // Default Background
pen.Background(nil)
case 58: // Set underline color
var c color.Color
n := ReadStyleColor(params[i:], &c)
if n > 0 {
pen.UnderlineColor(c)
i += n - 1
}
case 59: // Default underline color
pen.UnderlineColor(nil)
case 90, 91, 92, 93, 94, 95, 96, 97: // Set bright foreground
pen.Foreground(ansi.BrightBlack + ansi.BasicColor(param-90)) //nolint:gosec
case 100, 101, 102, 103, 104, 105, 106, 107: // Set bright background
pen.Background(ansi.BrightBlack + ansi.BasicColor(param-100)) //nolint:gosec
}
}
}
// ReadLink reads a hyperlink escape sequence from a data buffer.
func ReadLink(p []byte, link *Link) {
params := bytes.Split(p, []byte{';'})
if len(params) != 3 {
return
}
link.Params = string(params[1])
link.URL = string(params[2])
}
// ReadStyleColor reads a color from a list of parameters.
// See [ansi.ReadStyleColor] for more information.
func ReadStyleColor(params ansi.Params, c *color.Color) int {
return ansi.ReadStyleColor(params, c)
}

31
vendor/github.com/charmbracelet/x/cellbuf/style.go generated vendored Normal file
View File

@ -0,0 +1,31 @@
package cellbuf
import (
"github.com/charmbracelet/colorprofile"
)
// Convert converts a style to respect the given color profile.
func ConvertStyle(s Style, p colorprofile.Profile) Style {
switch p {
case colorprofile.TrueColor:
return s
case colorprofile.Ascii:
s.Fg = nil
s.Bg = nil
s.Ul = nil
case colorprofile.NoTTY:
return Style{}
}
if s.Fg != nil {
s.Fg = p.Convert(s.Fg)
}
if s.Bg != nil {
s.Bg = p.Convert(s.Bg)
}
if s.Ul != nil {
s.Ul = p.Convert(s.Ul)
}
return s
}

137
vendor/github.com/charmbracelet/x/cellbuf/tabstop.go generated vendored Normal file
View File

@ -0,0 +1,137 @@
package cellbuf
// DefaultTabInterval is the default tab interval.
const DefaultTabInterval = 8
// TabStops represents horizontal line tab stops.
type TabStops struct {
stops []int
interval int
width int
}
// NewTabStops creates a new set of tab stops from a number of columns and an
// interval.
func NewTabStops(width, interval int) *TabStops {
ts := new(TabStops)
ts.interval = interval
ts.width = width
ts.stops = make([]int, (width+(interval-1))/interval)
ts.init(0, width)
return ts
}
// DefaultTabStops creates a new set of tab stops with the default interval.
func DefaultTabStops(cols int) *TabStops {
return NewTabStops(cols, DefaultTabInterval)
}
// Resize resizes the tab stops to the given width.
func (ts *TabStops) Resize(width int) {
if width == ts.width {
return
}
if width < ts.width {
size := (width + (ts.interval - 1)) / ts.interval
ts.stops = ts.stops[:size]
} else {
size := (width - ts.width + (ts.interval - 1)) / ts.interval
ts.stops = append(ts.stops, make([]int, size)...)
}
ts.init(ts.width, width)
ts.width = width
}
// IsStop returns true if the given column is a tab stop.
func (ts TabStops) IsStop(col int) bool {
mask := ts.mask(col)
i := col >> 3
if i < 0 || i >= len(ts.stops) {
return false
}
return ts.stops[i]&mask != 0
}
// Next returns the next tab stop after the given column.
func (ts TabStops) Next(col int) int {
return ts.Find(col, 1)
}
// Prev returns the previous tab stop before the given column.
func (ts TabStops) Prev(col int) int {
return ts.Find(col, -1)
}
// Find returns the prev/next tab stop before/after the given column and delta.
// If delta is positive, it returns the next tab stop after the given column.
// If delta is negative, it returns the previous tab stop before the given column.
// If delta is zero, it returns the given column.
func (ts TabStops) Find(col, delta int) int {
if delta == 0 {
return col
}
var prev bool
count := delta
if count < 0 {
count = -count
prev = true
}
for count > 0 {
if !prev {
if col >= ts.width-1 {
return col
}
col++
} else {
if col < 1 {
return col
}
col--
}
if ts.IsStop(col) {
count--
}
}
return col
}
// Set adds a tab stop at the given column.
func (ts *TabStops) Set(col int) {
mask := ts.mask(col)
ts.stops[col>>3] |= mask
}
// Reset removes the tab stop at the given column.
func (ts *TabStops) Reset(col int) {
mask := ts.mask(col)
ts.stops[col>>3] &= ^mask
}
// Clear removes all tab stops.
func (ts *TabStops) Clear() {
ts.stops = make([]int, len(ts.stops))
}
// mask returns the mask for the given column.
func (ts *TabStops) mask(col int) int {
return 1 << (col & (ts.interval - 1))
}
// init initializes the tab stops starting from col until width.
func (ts *TabStops) init(col, width int) {
for x := col; x < width; x++ {
if x%ts.interval == 0 {
ts.Set(x)
} else {
ts.Reset(x)
}
}
}

38
vendor/github.com/charmbracelet/x/cellbuf/utils.go generated vendored Normal file
View File

@ -0,0 +1,38 @@
package cellbuf
import (
"strings"
)
// Height returns the height of a string.
func Height(s string) int {
return strings.Count(s, "\n") + 1
}
func min(a, b int) int { //nolint:predeclared
if a > b {
return b
}
return a
}
func max(a, b int) int { //nolint:predeclared
if a > b {
return a
}
return b
}
func clamp(v, low, high int) int {
if high < low {
low, high = high, low
}
return min(high, max(low, v))
}
func abs(a int) int {
if a < 0 {
return -a
}
return a
}

185
vendor/github.com/charmbracelet/x/cellbuf/wrap.go generated vendored Normal file
View File

@ -0,0 +1,185 @@
package cellbuf
import (
"bytes"
"unicode"
"unicode/utf8"
"github.com/charmbracelet/x/ansi"
)
const nbsp = '\u00a0'
// Wrap returns a string that is wrapped to the specified limit applying any
// ANSI escape sequences in the string. It tries to wrap the string at word
// boundaries, but will break words if necessary.
//
// The breakpoints string is a list of characters that are considered
// breakpoints for word wrapping. A hyphen (-) is always considered a
// breakpoint.
//
// Note: breakpoints must be a string of 1-cell wide rune characters.
func Wrap(s string, limit int, breakpoints string) string {
if len(s) == 0 {
return ""
}
if limit < 1 {
return s
}
p := ansi.GetParser()
defer ansi.PutParser(p)
var (
buf bytes.Buffer
word bytes.Buffer
space bytes.Buffer
style, curStyle Style
link, curLink Link
curWidth int
wordLen int
)
hasBlankStyle := func() bool {
// Only follow reverse attribute, bg color and underline style
return !style.Attrs.Contains(ReverseAttr) && style.Bg == nil && style.UlStyle == NoUnderline
}
addSpace := func() {
curWidth += space.Len()
buf.Write(space.Bytes())
space.Reset()
}
addWord := func() {
if word.Len() == 0 {
return
}
curLink = link
curStyle = style
addSpace()
curWidth += wordLen
buf.Write(word.Bytes())
word.Reset()
wordLen = 0
}
addNewline := func() {
if !curStyle.Empty() {
buf.WriteString(ansi.ResetStyle)
}
if !curLink.Empty() {
buf.WriteString(ansi.ResetHyperlink())
}
buf.WriteByte('\n')
if !curLink.Empty() {
buf.WriteString(ansi.SetHyperlink(curLink.URL, curLink.Params))
}
if !curStyle.Empty() {
buf.WriteString(curStyle.Sequence())
}
curWidth = 0
space.Reset()
}
var state byte
for len(s) > 0 {
seq, width, n, newState := ansi.DecodeSequence(s, state, p)
switch width {
case 0:
if ansi.Equal(seq, "\t") {
addWord()
space.WriteString(seq)
break
} else if ansi.Equal(seq, "\n") {
if wordLen == 0 {
if curWidth+space.Len() > limit {
curWidth = 0
} else {
// preserve whitespaces
buf.Write(space.Bytes())
}
space.Reset()
}
addWord()
addNewline()
break
} else if ansi.HasCsiPrefix(seq) && p.Command() == 'm' {
// SGR style sequence [ansi.SGR]
ReadStyle(p.Params(), &style)
} else if ansi.HasOscPrefix(seq) && p.Command() == 8 {
// Hyperlink sequence [ansi.SetHyperlink]
ReadLink(p.Data(), &link)
}
word.WriteString(seq)
default:
if len(seq) == 1 {
// ASCII
r, _ := utf8.DecodeRuneInString(seq)
if r != nbsp && unicode.IsSpace(r) && hasBlankStyle() {
addWord()
space.WriteRune(r)
break
} else if r == '-' || runeContainsAny(r, breakpoints) {
addSpace()
if curWidth+wordLen+width <= limit {
addWord()
buf.WriteString(seq)
curWidth += width
break
}
}
}
if wordLen+width > limit {
// Hardwrap the word if it's too long
addWord()
}
word.WriteString(seq)
wordLen += width
if curWidth+wordLen+space.Len() > limit {
addNewline()
}
}
s = s[n:]
state = newState
}
if wordLen == 0 {
if curWidth+space.Len() > limit {
curWidth = 0
} else {
// preserve whitespaces
buf.Write(space.Bytes())
}
space.Reset()
}
addWord()
if !curLink.Empty() {
buf.WriteString(ansi.ResetHyperlink())
}
if !curStyle.Empty() {
buf.WriteString(ansi.ResetStyle)
}
return buf.String()
}
func runeContainsAny[T string | []rune](r rune, s T) bool {
for _, c := range []rune(s) {
if c == r {
return true
}
}
return false
}

339
vendor/github.com/charmbracelet/x/cellbuf/writer.go generated vendored Normal file
View File

@ -0,0 +1,339 @@
package cellbuf
import (
"bytes"
"fmt"
"strings"
"github.com/charmbracelet/x/ansi"
)
// CellBuffer is a cell buffer that represents a set of cells in a screen or a
// grid.
type CellBuffer interface {
// Cell returns the cell at the given position.
Cell(x, y int) *Cell
// SetCell sets the cell at the given position to the given cell. It
// returns whether the cell was set successfully.
SetCell(x, y int, c *Cell) bool
// Bounds returns the bounds of the cell buffer.
Bounds() Rectangle
}
// FillRect fills the rectangle within the cell buffer with the given cell.
// This will not fill cells outside the bounds of the cell buffer.
func FillRect(s CellBuffer, c *Cell, rect Rectangle) {
for y := rect.Min.Y; y < rect.Max.Y; y++ {
for x := rect.Min.X; x < rect.Max.X; x++ {
s.SetCell(x, y, c) //nolint:errcheck
}
}
}
// Fill fills the cell buffer with the given cell.
func Fill(s CellBuffer, c *Cell) {
FillRect(s, c, s.Bounds())
}
// ClearRect clears the rectangle within the cell buffer with blank cells.
func ClearRect(s CellBuffer, rect Rectangle) {
FillRect(s, nil, rect)
}
// Clear clears the cell buffer with blank cells.
func Clear(s CellBuffer) {
Fill(s, nil)
}
// SetContentRect clears the rectangle within the cell buffer with blank cells,
// and sets the given string as its content. If the height or width of the
// string exceeds the height or width of the cell buffer, it will be truncated.
func SetContentRect(s CellBuffer, str string, rect Rectangle) {
// Replace all "\n" with "\r\n" to ensure the cursor is reset to the start
// of the line. Make sure we don't replace "\r\n" with "\r\r\n".
str = strings.ReplaceAll(str, "\r\n", "\n")
str = strings.ReplaceAll(str, "\n", "\r\n")
ClearRect(s, rect)
printString(s, ansi.GraphemeWidth, rect.Min.X, rect.Min.Y, rect, str, true, "")
}
// SetContent clears the cell buffer with blank cells, and sets the given string
// as its content. If the height or width of the string exceeds the height or
// width of the cell buffer, it will be truncated.
func SetContent(s CellBuffer, str string) {
SetContentRect(s, str, s.Bounds())
}
// Render returns a string representation of the grid with ANSI escape sequences.
func Render(d CellBuffer) string {
var buf bytes.Buffer
height := d.Bounds().Dy()
for y := 0; y < height; y++ {
_, line := RenderLine(d, y)
buf.WriteString(line)
if y < height-1 {
buf.WriteString("\r\n")
}
}
return buf.String()
}
// RenderLine returns a string representation of the yth line of the grid along
// with the width of the line.
func RenderLine(d CellBuffer, n int) (w int, line string) {
var pen Style
var link Link
var buf bytes.Buffer
var pendingLine string
var pendingWidth int // this ignores space cells until we hit a non-space cell
writePending := func() {
// If there's no pending line, we don't need to do anything.
if len(pendingLine) == 0 {
return
}
buf.WriteString(pendingLine)
w += pendingWidth
pendingWidth = 0
pendingLine = ""
}
for x := 0; x < d.Bounds().Dx(); x++ {
if cell := d.Cell(x, n); cell != nil && cell.Width > 0 {
// Convert the cell's style and link to the given color profile.
cellStyle := cell.Style
cellLink := cell.Link
if cellStyle.Empty() && !pen.Empty() {
writePending()
buf.WriteString(ansi.ResetStyle) //nolint:errcheck
pen.Reset()
}
if !cellStyle.Equal(&pen) {
writePending()
seq := cellStyle.DiffSequence(pen)
buf.WriteString(seq) // nolint:errcheck
pen = cellStyle
}
// Write the URL escape sequence
if cellLink != link && link.URL != "" {
writePending()
buf.WriteString(ansi.ResetHyperlink()) //nolint:errcheck
link.Reset()
}
if cellLink != link {
writePending()
buf.WriteString(ansi.SetHyperlink(cellLink.URL, cellLink.Params)) //nolint:errcheck
link = cellLink
}
// We only write the cell content if it's not empty. If it is, we
// append it to the pending line and width to be evaluated later.
if cell.Equal(&BlankCell) {
pendingLine += cell.String()
pendingWidth += cell.Width
} else {
writePending()
buf.WriteString(cell.String())
w += cell.Width
}
}
}
if link.URL != "" {
buf.WriteString(ansi.ResetHyperlink()) //nolint:errcheck
}
if !pen.Empty() {
buf.WriteString(ansi.ResetStyle) //nolint:errcheck
}
return w, strings.TrimRight(buf.String(), " ") // Trim trailing spaces
}
// ScreenWriter represents a writer that writes to a [Screen] parsing ANSI
// escape sequences and Unicode characters and converting them into cells that
// can be written to a cell [Buffer].
type ScreenWriter struct {
*Screen
}
// NewScreenWriter creates a new ScreenWriter that writes to the given Screen.
// This is a convenience function for creating a ScreenWriter.
func NewScreenWriter(s *Screen) *ScreenWriter {
return &ScreenWriter{s}
}
// Write writes the given bytes to the screen.
// This will recognize ANSI [ansi.SGR] style and [ansi.SetHyperlink] escape
// sequences.
func (s *ScreenWriter) Write(p []byte) (n int, err error) {
printString(s.Screen, s.method,
s.cur.X, s.cur.Y, s.Bounds(),
p, false, "")
return len(p), nil
}
// SetContent clears the screen with blank cells, and sets the given string as
// its content. If the height or width of the string exceeds the height or
// width of the screen, it will be truncated.
//
// This will recognize ANSI [ansi.SGR] style and [ansi.SetHyperlink] escape sequences.
func (s *ScreenWriter) SetContent(str string) {
s.SetContentRect(str, s.Bounds())
}
// SetContentRect clears the rectangle within the screen with blank cells, and
// sets the given string as its content. If the height or width of the string
// exceeds the height or width of the screen, it will be truncated.
//
// This will recognize ANSI [ansi.SGR] style and [ansi.SetHyperlink] escape
// sequences.
func (s *ScreenWriter) SetContentRect(str string, rect Rectangle) {
// Replace all "\n" with "\r\n" to ensure the cursor is reset to the start
// of the line. Make sure we don't replace "\r\n" with "\r\r\n".
str = strings.ReplaceAll(str, "\r\n", "\n")
str = strings.ReplaceAll(str, "\n", "\r\n")
s.ClearRect(rect)
printString(s.Screen, s.method,
rect.Min.X, rect.Min.Y, rect,
str, true, "")
}
// Print prints the string at the current cursor position. It will wrap the
// string to the width of the screen if it exceeds the width of the screen.
// This will recognize ANSI [ansi.SGR] style and [ansi.SetHyperlink] escape
// sequences.
func (s *ScreenWriter) Print(str string, v ...interface{}) {
if len(v) > 0 {
str = fmt.Sprintf(str, v...)
}
printString(s.Screen, s.method,
s.cur.X, s.cur.Y, s.Bounds(),
str, false, "")
}
// PrintAt prints the string at the given position. It will wrap the string to
// the width of the screen if it exceeds the width of the screen.
// This will recognize ANSI [ansi.SGR] style and [ansi.SetHyperlink] escape
// sequences.
func (s *ScreenWriter) PrintAt(x, y int, str string, v ...interface{}) {
if len(v) > 0 {
str = fmt.Sprintf(str, v...)
}
printString(s.Screen, s.method,
x, y, s.Bounds(),
str, false, "")
}
// PrintCrop prints the string at the current cursor position and truncates the
// text if it exceeds the width of the screen. Use tail to specify a string to
// append if the string is truncated.
// This will recognize ANSI [ansi.SGR] style and [ansi.SetHyperlink] escape
// sequences.
func (s *ScreenWriter) PrintCrop(str string, tail string) {
printString(s.Screen, s.method,
s.cur.X, s.cur.Y, s.Bounds(),
str, true, tail)
}
// PrintCropAt prints the string at the given position and truncates the text
// if it exceeds the width of the screen. Use tail to specify a string to append
// if the string is truncated.
// This will recognize ANSI [ansi.SGR] style and [ansi.SetHyperlink] escape
// sequences.
func (s *ScreenWriter) PrintCropAt(x, y int, str string, tail string) {
printString(s.Screen, s.method,
x, y, s.Bounds(),
str, true, tail)
}
// printString draws a string starting at the given position.
func printString[T []byte | string](
s CellBuffer,
m ansi.Method,
x, y int,
bounds Rectangle, str T,
truncate bool, tail string,
) {
p := ansi.GetParser()
defer ansi.PutParser(p)
var tailc Cell
if truncate && len(tail) > 0 {
if m == ansi.WcWidth {
tailc = *NewCellString(tail)
} else {
tailc = *NewGraphemeCell(tail)
}
}
decoder := ansi.DecodeSequenceWc[T]
if m == ansi.GraphemeWidth {
decoder = ansi.DecodeSequence[T]
}
var cell Cell
var style Style
var link Link
var state byte
for len(str) > 0 {
seq, width, n, newState := decoder(str, state, p)
switch width {
case 1, 2, 3, 4: // wide cells can go up to 4 cells wide
cell.Width += width
cell.Append([]rune(string(seq))...)
if !truncate && x+cell.Width > bounds.Max.X && y+1 < bounds.Max.Y {
// Wrap the string to the width of the window
x = bounds.Min.X
y++
}
if Pos(x, y).In(bounds) {
if truncate && tailc.Width > 0 && x+cell.Width > bounds.Max.X-tailc.Width {
// Truncate the string and append the tail if any.
cell := tailc
cell.Style = style
cell.Link = link
s.SetCell(x, y, &cell)
x += tailc.Width
} else {
// Print the cell to the screen
cell.Style = style
cell.Link = link
s.SetCell(x, y, &cell) //nolint:errcheck
x += width
}
}
// String is too long for the line, truncate it.
// Make sure we reset the cell for the next iteration.
cell.Reset()
default:
// Valid sequences always have a non-zero Cmd.
// TODO: Handle cursor movement and other sequences
switch {
case ansi.HasCsiPrefix(seq) && p.Command() == 'm':
// SGR - Select Graphic Rendition
ReadStyle(p.Params(), &style)
case ansi.HasOscPrefix(seq) && p.Command() == 8:
// Hyperlinks
ReadLink(p.Data(), &link)
case ansi.Equal(seq, T("\n")):
y++
case ansi.Equal(seq, T("\r")):
x = bounds.Min.X
default:
cell.Append([]rune(string(seq))...)
}
}
// Advance the state and data
state = newState
str = str[n:]
}
// Make sure to set the last cell if it's not empty.
if !cell.Empty() {
s.SetCell(x, y, &cell) //nolint:errcheck
cell.Reset()
}
}

21
vendor/github.com/charmbracelet/x/term/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Charmbracelet, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

49
vendor/github.com/charmbracelet/x/term/term.go generated vendored Normal file
View File

@ -0,0 +1,49 @@
package term
// State contains platform-specific state of a terminal.
type State struct {
state
}
// IsTerminal returns whether the given file descriptor is a terminal.
func IsTerminal(fd uintptr) bool {
return isTerminal(fd)
}
// MakeRaw puts the terminal connected to the given file descriptor into raw
// mode and returns the previous state of the terminal so that it can be
// restored.
func MakeRaw(fd uintptr) (*State, error) {
return makeRaw(fd)
}
// GetState returns the current state of a terminal which may be useful to
// restore the terminal after a signal.
func GetState(fd uintptr) (*State, error) {
return getState(fd)
}
// SetState sets the given state of the terminal.
func SetState(fd uintptr, state *State) error {
return setState(fd, state)
}
// Restore restores the terminal connected to the given file descriptor to a
// previous state.
func Restore(fd uintptr, oldState *State) error {
return restore(fd, oldState)
}
// GetSize returns the visible dimensions of the given terminal.
//
// These dimensions don't include any scrollback buffer height.
func GetSize(fd uintptr) (width, height int, err error) {
return getSize(fd)
}
// ReadPassword reads a line of input from a terminal without local echo. This
// is commonly used for inputting passwords and other sensitive data. The slice
// returned does not include the \n.
func ReadPassword(fd uintptr) ([]byte, error) {
return readPassword(fd)
}

39
vendor/github.com/charmbracelet/x/term/term_other.go generated vendored Normal file
View File

@ -0,0 +1,39 @@
//go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !zos && !windows && !solaris && !plan9
// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!zos,!windows,!solaris,!plan9
package term
import (
"fmt"
"runtime"
)
type state struct{}
func isTerminal(fd uintptr) bool {
return false
}
func makeRaw(fd uintptr) (*State, error) {
return nil, fmt.Errorf("terminal: MakeRaw not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}
func getState(fd uintptr) (*State, error) {
return nil, fmt.Errorf("terminal: GetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}
func restore(fd uintptr, state *State) error {
return fmt.Errorf("terminal: Restore not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}
func getSize(fd uintptr) (width, height int, err error) {
return 0, 0, fmt.Errorf("terminal: GetSize not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}
func setState(fd uintptr, state *State) error {
return fmt.Errorf("terminal: SetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}
func readPassword(fd uintptr) ([]byte, error) {
return nil, fmt.Errorf("terminal: ReadPassword not implemented on %s/%s", runtime.GOOS, runtime.GOARCH)
}

Some files were not shown because too many files have changed in this diff Show More