diff options
| author | mo khan <mo@mokhan.ca> | 2025-07-22 17:35:49 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-07-22 17:35:49 -0600 |
| commit | 20ef0d92694465ac86b550df139e8366a0a2b4fa (patch) | |
| tree | 3f14589e1ce6eb9306a3af31c3a1f9e1af5ed637 /vendor/github.com/charmbracelet/x | |
| parent | 44e0d272c040cdc53a98b9f1dc58ae7da67752e6 (diff) | |
feat: connect to spicedb
Diffstat (limited to 'vendor/github.com/charmbracelet/x')
74 files changed, 12416 insertions, 0 deletions
diff --git a/vendor/github.com/charmbracelet/x/ansi/LICENSE b/vendor/github.com/charmbracelet/x/ansi/LICENSE new file mode 100644 index 0000000..65a5654 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/LICENSE @@ -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. diff --git a/vendor/github.com/charmbracelet/x/ansi/ansi.go b/vendor/github.com/charmbracelet/x/ansi/ansi.go new file mode 100644 index 0000000..48d873c --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/ansi.go @@ -0,0 +1,11 @@ +package ansi + +import "io" + +// Execute is a function that "execute" the given escape sequence by writing it +// to the provided output writter. +// +// This is a syntactic sugar over [io.WriteString]. +func Execute(w io.Writer, s string) (int, error) { + return io.WriteString(w, s) +} diff --git a/vendor/github.com/charmbracelet/x/ansi/ascii.go b/vendor/github.com/charmbracelet/x/ansi/ascii.go new file mode 100644 index 0000000..188582f --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/ascii.go @@ -0,0 +1,8 @@ +package ansi + +const ( + // SP is the space character (Char: \x20). + SP = 0x20 + // DEL is the delete character (Caret: ^?, Char: \x7f). + DEL = 0x7F +) diff --git a/vendor/github.com/charmbracelet/x/ansi/background.go b/vendor/github.com/charmbracelet/x/ansi/background.go new file mode 100644 index 0000000..2383cf0 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/background.go @@ -0,0 +1,169 @@ +package ansi + +import ( + "fmt" + "image/color" +) + +// Colorizer is a [color.Color] interface that can be formatted as a string. +type Colorizer interface { + color.Color + fmt.Stringer +} + +// HexColorizer is a [color.Color] that can be formatted as a hex string. +type HexColorizer struct{ color.Color } + +var _ Colorizer = HexColorizer{} + +// String returns the color as a hex string. If the color is nil, an empty +// string is returned. +func (h HexColorizer) String() string { + if h.Color == nil { + return "" + } + r, g, b, _ := h.RGBA() + // Get the lower 8 bits + r &= 0xff + g &= 0xff + b &= 0xff + return fmt.Sprintf("#%02x%02x%02x", uint8(r), uint8(g), uint8(b)) //nolint:gosec +} + +// XRGBColorizer is a [color.Color] that can be formatted as an XParseColor +// rgb: string. +// +// See: https://linux.die.net/man/3/xparsecolor +type XRGBColorizer struct{ color.Color } + +var _ Colorizer = XRGBColorizer{} + +// String returns the color as an XParseColor rgb: string. If the color is nil, +// an empty string is returned. +func (x XRGBColorizer) String() string { + if x.Color == nil { + return "" + } + r, g, b, _ := x.RGBA() + // Get the lower 8 bits + return fmt.Sprintf("rgb:%04x/%04x/%04x", r, g, b) +} + +// XRGBAColorizer is a [color.Color] that can be formatted as an XParseColor +// rgba: string. +// +// See: https://linux.die.net/man/3/xparsecolor +type XRGBAColorizer struct{ color.Color } + +var _ Colorizer = XRGBAColorizer{} + +// String returns the color as an XParseColor rgba: string. If the color is nil, +// an empty string is returned. +func (x XRGBAColorizer) String() string { + if x.Color == nil { + return "" + } + r, g, b, a := x.RGBA() + // Get the lower 8 bits + return fmt.Sprintf("rgba:%04x/%04x/%04x/%04x", r, g, b, a) +} + +// SetForegroundColor returns a sequence that sets the default terminal +// foreground color. +// +// OSC 10 ; color ST +// OSC 10 ; color BEL +// +// Where color is the encoded color number. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func SetForegroundColor(c color.Color) string { + var s string + switch c := c.(type) { + case Colorizer: + s = c.String() + case fmt.Stringer: + s = c.String() + default: + s = HexColorizer{c}.String() + } + return "\x1b]10;" + s + "\x07" +} + +// RequestForegroundColor is a sequence that requests the current default +// terminal foreground color. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +const RequestForegroundColor = "\x1b]10;?\x07" + +// ResetForegroundColor is a sequence that resets the default terminal +// foreground color. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +const ResetForegroundColor = "\x1b]110\x07" + +// SetBackgroundColor returns a sequence that sets the default terminal +// background color. +// +// OSC 11 ; color ST +// OSC 11 ; color BEL +// +// Where color is the encoded color number. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func SetBackgroundColor(c color.Color) string { + var s string + switch c := c.(type) { + case Colorizer: + s = c.String() + case fmt.Stringer: + s = c.String() + default: + s = HexColorizer{c}.String() + } + return "\x1b]11;" + s + "\x07" +} + +// RequestBackgroundColor is a sequence that requests the current default +// terminal background color. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +const RequestBackgroundColor = "\x1b]11;?\x07" + +// ResetBackgroundColor is a sequence that resets the default terminal +// background color. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +const ResetBackgroundColor = "\x1b]111\x07" + +// SetCursorColor returns a sequence that sets the terminal cursor color. +// +// OSC 12 ; color ST +// OSC 12 ; color BEL +// +// Where color is the encoded color number. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func SetCursorColor(c color.Color) string { + var s string + switch c := c.(type) { + case Colorizer: + s = c.String() + case fmt.Stringer: + s = c.String() + default: + s = HexColorizer{c}.String() + } + return "\x1b]12;" + s + "\x07" +} + +// RequestCursorColor is a sequence that requests the current terminal cursor +// color. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +const RequestCursorColor = "\x1b]12;?\x07" + +// ResetCursorColor is a sequence that resets the terminal cursor color. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +const ResetCursorColor = "\x1b]112\x07" diff --git a/vendor/github.com/charmbracelet/x/ansi/c0.go b/vendor/github.com/charmbracelet/x/ansi/c0.go new file mode 100644 index 0000000..28ff7c2 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/c0.go @@ -0,0 +1,79 @@ +package ansi + +// C0 control characters. +// +// These range from (0x00-0x1F) as defined in ISO 646 (ASCII). +// See: https://en.wikipedia.org/wiki/C0_and_C1_control_codes +const ( + // NUL is the null character (Caret: ^@, Char: \0). + NUL = 0x00 + // SOH is the start of heading character (Caret: ^A). + SOH = 0x01 + // STX is the start of text character (Caret: ^B). + STX = 0x02 + // ETX is the end of text character (Caret: ^C). + ETX = 0x03 + // EOT is the end of transmission character (Caret: ^D). + EOT = 0x04 + // ENQ is the enquiry character (Caret: ^E). + ENQ = 0x05 + // ACK is the acknowledge character (Caret: ^F). + ACK = 0x06 + // BEL is the bell character (Caret: ^G, Char: \a). + BEL = 0x07 + // BS is the backspace character (Caret: ^H, Char: \b). + BS = 0x08 + // HT is the horizontal tab character (Caret: ^I, Char: \t). + HT = 0x09 + // LF is the line feed character (Caret: ^J, Char: \n). + LF = 0x0A + // VT is the vertical tab character (Caret: ^K, Char: \v). + VT = 0x0B + // FF is the form feed character (Caret: ^L, Char: \f). + FF = 0x0C + // CR is the carriage return character (Caret: ^M, Char: \r). + CR = 0x0D + // SO is the shift out character (Caret: ^N). + SO = 0x0E + // SI is the shift in character (Caret: ^O). + SI = 0x0F + // DLE is the data link escape character (Caret: ^P). + DLE = 0x10 + // DC1 is the device control 1 character (Caret: ^Q). + DC1 = 0x11 + // DC2 is the device control 2 character (Caret: ^R). + DC2 = 0x12 + // DC3 is the device control 3 character (Caret: ^S). + DC3 = 0x13 + // DC4 is the device control 4 character (Caret: ^T). + DC4 = 0x14 + // NAK is the negative acknowledge character (Caret: ^U). + NAK = 0x15 + // SYN is the synchronous idle character (Caret: ^V). + SYN = 0x16 + // ETB is the end of transmission block character (Caret: ^W). + ETB = 0x17 + // CAN is the cancel character (Caret: ^X). + CAN = 0x18 + // EM is the end of medium character (Caret: ^Y). + EM = 0x19 + // SUB is the substitute character (Caret: ^Z). + SUB = 0x1A + // ESC is the escape character (Caret: ^[, Char: \e). + ESC = 0x1B + // FS is the file separator character (Caret: ^\). + FS = 0x1C + // GS is the group separator character (Caret: ^]). + GS = 0x1D + // RS is the record separator character (Caret: ^^). + RS = 0x1E + // US is the unit separator character (Caret: ^_). + US = 0x1F + + // LS0 is the locking shift 0 character. + // This is an alias for [SI]. + LS0 = SI + // LS1 is the locking shift 1 character. + // This is an alias for [SO]. + LS1 = SO +) diff --git a/vendor/github.com/charmbracelet/x/ansi/c1.go b/vendor/github.com/charmbracelet/x/ansi/c1.go new file mode 100644 index 0000000..71058f5 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/c1.go @@ -0,0 +1,72 @@ +package ansi + +// C1 control characters. +// +// These range from (0x80-0x9F) as defined in ISO 6429 (ECMA-48). +// See: https://en.wikipedia.org/wiki/C0_and_C1_control_codes +const ( + // PAD is the padding character. + PAD = 0x80 + // HOP is the high octet preset character. + HOP = 0x81 + // BPH is the break permitted here character. + BPH = 0x82 + // NBH is the no break here character. + NBH = 0x83 + // IND is the index character. + IND = 0x84 + // NEL is the next line character. + NEL = 0x85 + // SSA is the start of selected area character. + SSA = 0x86 + // ESA is the end of selected area character. + ESA = 0x87 + // HTS is the horizontal tab set character. + HTS = 0x88 + // HTJ is the horizontal tab with justification character. + HTJ = 0x89 + // VTS is the vertical tab set character. + VTS = 0x8A + // PLD is the partial line forward character. + PLD = 0x8B + // PLU is the partial line backward character. + PLU = 0x8C + // RI is the reverse index character. + RI = 0x8D + // SS2 is the single shift 2 character. + SS2 = 0x8E + // SS3 is the single shift 3 character. + SS3 = 0x8F + // DCS is the device control string character. + DCS = 0x90 + // PU1 is the private use 1 character. + PU1 = 0x91 + // PU2 is the private use 2 character. + PU2 = 0x92 + // STS is the set transmit state character. + STS = 0x93 + // CCH is the cancel character. + CCH = 0x94 + // MW is the message waiting character. + MW = 0x95 + // SPA is the start of guarded area character. + SPA = 0x96 + // EPA is the end of guarded area character. + EPA = 0x97 + // SOS is the start of string character. + SOS = 0x98 + // SGCI is the single graphic character introducer character. + SGCI = 0x99 + // SCI is the single character introducer character. + SCI = 0x9A + // CSI is the control sequence introducer character. + CSI = 0x9B + // ST is the string terminator character. + ST = 0x9C + // OSC is the operating system command character. + OSC = 0x9D + // PM is the privacy message character. + PM = 0x9E + // APC is the application program command character. + APC = 0x9F +) diff --git a/vendor/github.com/charmbracelet/x/ansi/charset.go b/vendor/github.com/charmbracelet/x/ansi/charset.go new file mode 100644 index 0000000..50fff51 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/charset.go @@ -0,0 +1,55 @@ +package ansi + +// SelectCharacterSet sets the G-set character designator to the specified +// character set. +// +// ESC Ps Pd +// +// Where Ps is the G-set character designator, and Pd is the identifier. +// For 94-character sets, the designator can be one of: +// - ( G0 +// - ) G1 +// - * G2 +// - + G3 +// +// For 96-character sets, the designator can be one of: +// - - G1 +// - . G2 +// - / G3 +// +// Some common 94-character sets are: +// - 0 DEC Special Drawing Set +// - A United Kingdom (UK) +// - B United States (USASCII) +// +// Examples: +// +// ESC ( B Select character set G0 = United States (USASCII) +// ESC ( 0 Select character set G0 = Special Character and Line Drawing Set +// ESC ) 0 Select character set G1 = Special Character and Line Drawing Set +// ESC * A Select character set G2 = United Kingdom (UK) +// +// See: https://vt100.net/docs/vt510-rm/SCS.html +func SelectCharacterSet(gset byte, charset byte) string { + return "\x1b" + string(gset) + string(charset) +} + +// SCS is an alias for SelectCharacterSet. +func SCS(gset byte, charset byte) string { + return SelectCharacterSet(gset, charset) +} + +// Locking Shift 1 Right (LS1R) shifts G1 into GR character set. +const LS1R = "\x1b~" + +// Locking Shift 2 (LS2) shifts G2 into GL character set. +const LS2 = "\x1bn" + +// Locking Shift 2 Right (LS2R) shifts G2 into GR character set. +const LS2R = "\x1b}" + +// Locking Shift 3 (LS3) shifts G3 into GL character set. +const LS3 = "\x1bo" + +// Locking Shift 3 Right (LS3R) shifts G3 into GR character set. +const LS3R = "\x1b|" diff --git a/vendor/github.com/charmbracelet/x/ansi/clipboard.go b/vendor/github.com/charmbracelet/x/ansi/clipboard.go new file mode 100644 index 0000000..94d26c3 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/clipboard.go @@ -0,0 +1,75 @@ +package ansi + +import "encoding/base64" + +// Clipboard names. +const ( + SystemClipboard = 'c' + PrimaryClipboard = 'p' +) + +// SetClipboard returns a sequence for manipulating the clipboard. +// +// OSC 52 ; Pc ; Pd ST +// OSC 52 ; Pc ; Pd BEL +// +// Where Pc is the clipboard name and Pd is the base64 encoded data. +// Empty data or invalid base64 data will reset the clipboard. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func SetClipboard(c byte, d string) string { + if d != "" { + d = base64.StdEncoding.EncodeToString([]byte(d)) + } + return "\x1b]52;" + string(c) + ";" + d + "\x07" +} + +// SetSystemClipboard returns a sequence for setting the system clipboard. +// +// This is equivalent to SetClipboard(SystemClipboard, d). +func SetSystemClipboard(d string) string { + return SetClipboard(SystemClipboard, d) +} + +// SetPrimaryClipboard returns a sequence for setting the primary clipboard. +// +// This is equivalent to SetClipboard(PrimaryClipboard, d). +func SetPrimaryClipboard(d string) string { + return SetClipboard(PrimaryClipboard, d) +} + +// ResetClipboard returns a sequence for resetting the clipboard. +// +// This is equivalent to SetClipboard(c, ""). +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func ResetClipboard(c byte) string { + return SetClipboard(c, "") +} + +// ResetSystemClipboard is a sequence for resetting the system clipboard. +// +// This is equivalent to ResetClipboard(SystemClipboard). +const ResetSystemClipboard = "\x1b]52;c;\x07" + +// ResetPrimaryClipboard is a sequence for resetting the primary clipboard. +// +// This is equivalent to ResetClipboard(PrimaryClipboard). +const ResetPrimaryClipboard = "\x1b]52;p;\x07" + +// RequestClipboard returns a sequence for requesting the clipboard. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func RequestClipboard(c byte) string { + return "\x1b]52;" + string(c) + ";?\x07" +} + +// RequestSystemClipboard is a sequence for requesting the system clipboard. +// +// This is equivalent to RequestClipboard(SystemClipboard). +const RequestSystemClipboard = "\x1b]52;c;?\x07" + +// RequestPrimaryClipboard is a sequence for requesting the primary clipboard. +// +// This is equivalent to RequestClipboard(PrimaryClipboard). +const RequestPrimaryClipboard = "\x1b]52;p;?\x07" diff --git a/vendor/github.com/charmbracelet/x/ansi/color.go b/vendor/github.com/charmbracelet/x/ansi/color.go new file mode 100644 index 0000000..77f8a08 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/color.go @@ -0,0 +1,196 @@ +package ansi + +import ( + "image/color" +) + +// Technically speaking, the 16 basic ANSI colors are arbitrary and can be +// customized at the terminal level. Given that, we're returning what we feel +// are good defaults. +// +// This could also be a slice, but we use a map to make the mappings very +// explicit. +// +// See: https://www.ditig.com/publications/256-colors-cheat-sheet +var lowANSI = map[uint32]uint32{ + 0: 0x000000, // black + 1: 0x800000, // red + 2: 0x008000, // green + 3: 0x808000, // yellow + 4: 0x000080, // blue + 5: 0x800080, // magenta + 6: 0x008080, // cyan + 7: 0xc0c0c0, // white + 8: 0x808080, // bright black + 9: 0xff0000, // bright red + 10: 0x00ff00, // bright green + 11: 0xffff00, // bright yellow + 12: 0x0000ff, // bright blue + 13: 0xff00ff, // bright magenta + 14: 0x00ffff, // bright cyan + 15: 0xffffff, // bright white +} + +// Color is a color that can be used in a terminal. ANSI (including +// ANSI256) and 24-bit "true colors" fall under this category. +type Color interface { + color.Color +} + +// BasicColor is an ANSI 3-bit or 4-bit color with a value from 0 to 15. +type BasicColor uint8 + +var _ Color = BasicColor(0) + +const ( + // Black is the ANSI black color. + Black BasicColor = iota + + // Red is the ANSI red color. + Red + + // Green is the ANSI green color. + Green + + // Yellow is the ANSI yellow color. + Yellow + + // Blue is the ANSI blue color. + Blue + + // Magenta is the ANSI magenta color. + Magenta + + // Cyan is the ANSI cyan color. + Cyan + + // White is the ANSI white color. + White + + // BrightBlack is the ANSI bright black color. + BrightBlack + + // BrightRed is the ANSI bright red color. + BrightRed + + // BrightGreen is the ANSI bright green color. + BrightGreen + + // BrightYellow is the ANSI bright yellow color. + BrightYellow + + // BrightBlue is the ANSI bright blue color. + BrightBlue + + // BrightMagenta is the ANSI bright magenta color. + BrightMagenta + + // BrightCyan is the ANSI bright cyan color. + BrightCyan + + // BrightWhite is the ANSI bright white color. + BrightWhite +) + +// RGBA returns the red, green, blue and alpha components of the color. It +// satisfies the color.Color interface. +func (c BasicColor) RGBA() (uint32, uint32, uint32, uint32) { + ansi := uint32(c) + if ansi > 15 { + return 0, 0, 0, 0xffff + } + + r, g, b := ansiToRGB(ansi) + return toRGBA(r, g, b) +} + +// ExtendedColor is an ANSI 256 (8-bit) color with a value from 0 to 255. +type ExtendedColor uint8 + +var _ Color = ExtendedColor(0) + +// RGBA returns the red, green, blue and alpha components of the color. It +// satisfies the color.Color interface. +func (c ExtendedColor) RGBA() (uint32, uint32, uint32, uint32) { + r, g, b := ansiToRGB(uint32(c)) + return toRGBA(r, g, b) +} + +// TrueColor is a 24-bit color that can be used in the terminal. +// This can be used to represent RGB colors. +// +// For example, the color red can be represented as: +// +// TrueColor(0xff0000) +type TrueColor uint32 + +var _ Color = TrueColor(0) + +// RGBA returns the red, green, blue and alpha components of the color. It +// satisfies the color.Color interface. +func (c TrueColor) RGBA() (uint32, uint32, uint32, uint32) { + r, g, b := hexToRGB(uint32(c)) + return toRGBA(r, g, b) +} + +// ansiToRGB converts an ANSI color to a 24-bit RGB color. +// +// r, g, b := ansiToRGB(57) +func ansiToRGB(ansi uint32) (uint32, uint32, uint32) { + // For out-of-range values return black. + if ansi > 255 { + return 0, 0, 0 + } + + // Low ANSI. + if ansi < 16 { + h, ok := lowANSI[ansi] + if !ok { + return 0, 0, 0 + } + r, g, b := hexToRGB(h) + return r, g, b + } + + // Grays. + if ansi > 231 { + s := (ansi-232)*10 + 8 + return s, s, s + } + + // ANSI256. + n := ansi - 16 + b := n % 6 + g := (n - b) / 6 % 6 + r := (n - b - g*6) / 36 % 6 + for _, v := range []*uint32{&r, &g, &b} { + if *v > 0 { + c := *v*40 + 55 + *v = c + } + } + + return r, g, b +} + +// hexToRGB converts a number in hexadecimal format to red, green, and blue +// values. +// +// r, g, b := hexToRGB(0x0000FF) +func hexToRGB(hex uint32) (uint32, uint32, uint32) { + return hex >> 16 & 0xff, hex >> 8 & 0xff, hex & 0xff +} + +// toRGBA converts an RGB 8-bit color values to 32-bit color values suitable +// for color.Color. +// +// color.Color requires 16-bit color values, so we duplicate the 8-bit values +// to fill the 16-bit values. +// +// This always returns 0xffff (opaque) for the alpha channel. +func toRGBA(r, g, b uint32) (uint32, uint32, uint32, uint32) { + r |= r << 8 + g |= g << 8 + b |= b << 8 + return r, g, b, 0xffff +} diff --git a/vendor/github.com/charmbracelet/x/ansi/ctrl.go b/vendor/github.com/charmbracelet/x/ansi/ctrl.go new file mode 100644 index 0000000..8ca744c --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/ctrl.go @@ -0,0 +1,137 @@ +package ansi + +import ( + "strconv" + "strings" +) + +// RequestNameVersion (XTVERSION) is a control sequence that requests the +// terminal's name and version. It responds with a DSR sequence identifying the +// terminal. +// +// CSI > 0 q +// DCS > | text ST +// +// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-PC-Style-Function-Keys +const ( + RequestNameVersion = "\x1b[>q" + XTVERSION = RequestNameVersion +) + +// RequestXTVersion is a control sequence that requests the terminal's XTVERSION. It responds with a DSR sequence identifying the version. +// +// CSI > Ps q +// DCS > | text ST +// +// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-PC-Style-Function-Keys +// +// Deprecated: use [RequestNameVersion] instead. +const RequestXTVersion = RequestNameVersion + +// PrimaryDeviceAttributes (DA1) is a control sequence that reports the +// terminal's primary device attributes. +// +// CSI c +// CSI 0 c +// CSI ? Ps ; ... c +// +// If no attributes are given, or if the attribute is 0, this function returns +// the request sequence. Otherwise, it returns the response sequence. +// +// See https://vt100.net/docs/vt510-rm/DA1.html +func PrimaryDeviceAttributes(attrs ...int) string { + if len(attrs) == 0 { + return RequestPrimaryDeviceAttributes + } else if len(attrs) == 1 && attrs[0] == 0 { + return "\x1b[0c" + } + + as := make([]string, len(attrs)) + for i, a := range attrs { + as[i] = strconv.Itoa(a) + } + return "\x1b[?" + strings.Join(as, ";") + "c" +} + +// DA1 is an alias for [PrimaryDeviceAttributes]. +func DA1(attrs ...int) string { + return PrimaryDeviceAttributes(attrs...) +} + +// RequestPrimaryDeviceAttributes is a control sequence that requests the +// terminal's primary device attributes (DA1). +// +// CSI c +// +// See https://vt100.net/docs/vt510-rm/DA1.html +const RequestPrimaryDeviceAttributes = "\x1b[c" + +// SecondaryDeviceAttributes (DA2) is a control sequence that reports the +// terminal's secondary device attributes. +// +// CSI > c +// CSI > 0 c +// CSI > Ps ; ... c +// +// See https://vt100.net/docs/vt510-rm/DA2.html +func SecondaryDeviceAttributes(attrs ...int) string { + if len(attrs) == 0 { + return RequestSecondaryDeviceAttributes + } + + as := make([]string, len(attrs)) + for i, a := range attrs { + as[i] = strconv.Itoa(a) + } + return "\x1b[>" + strings.Join(as, ";") + "c" +} + +// DA2 is an alias for [SecondaryDeviceAttributes]. +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. +// +// CSI = c +// CSI = 0 c +// DCS ! | Text ST +// +// Where Text is the unit ID for the terminal. +// +// If no unit ID is given, or if the unit ID is 0, this function returns the +// request sequence. Otherwise, it returns the response sequence. +// +// See https://vt100.net/docs/vt510-rm/DA3.html +func TertiaryDeviceAttributes(unitID string) string { + switch unitID { + case "": + return RequestTertiaryDeviceAttributes + case "0": + return "\x1b[=0c" + } + + return "\x1bP!|" + unitID + "\x1b\\" +} + +// DA3 is an alias for [TertiaryDeviceAttributes]. +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" diff --git a/vendor/github.com/charmbracelet/x/ansi/cursor.go b/vendor/github.com/charmbracelet/x/ansi/cursor.go new file mode 100644 index 0000000..0c364d6 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/cursor.go @@ -0,0 +1,633 @@ +package ansi + +import "strconv" + +// SaveCursor (DECSC) is an escape sequence that saves the current cursor +// position. +// +// ESC 7 +// +// See: https://vt100.net/docs/vt510-rm/DECSC.html +const ( + SaveCursor = "\x1b7" + DECSC = SaveCursor +) + +// RestoreCursor (DECRC) is an escape sequence that restores the cursor +// position. +// +// ESC 8 +// +// See: https://vt100.net/docs/vt510-rm/DECRC.html +const ( + RestoreCursor = "\x1b8" + DECRC = RestoreCursor +) + +// RequestCursorPosition 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 +// +// Deprecated: use [RequestCursorPositionReport] instead. +const RequestCursorPosition = "\x1b[6n" + +// RequestExtendedCursorPosition (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 +// +// Deprecated: use [RequestExtendedCursorPositionReport] instead. +const RequestExtendedCursorPosition = "\x1b[?6n" + +// CursorUp (CUU) returns a sequence for moving the cursor up n cells. +// +// CSI n A +// +// See: https://vt100.net/docs/vt510-rm/CUU.html +func CursorUp(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "A" +} + +// CUU is an alias for [CursorUp]. +func CUU(n int) string { + return CursorUp(n) +} + +// CUU1 is a sequence for moving the cursor up one cell. +const CUU1 = "\x1b[A" + +// CursorUp1 is a sequence for moving the cursor up one cell. +// +// This is equivalent to CursorUp(1). +// +// Deprecated: use [CUU1] instead. +const CursorUp1 = "\x1b[A" + +// CursorDown (CUD) returns a sequence for moving the cursor down n cells. +// +// CSI n B +// +// See: https://vt100.net/docs/vt510-rm/CUD.html +func CursorDown(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "B" +} + +// CUD is an alias for [CursorDown]. +func CUD(n int) string { + return CursorDown(n) +} + +// CUD1 is a sequence for moving the cursor down one cell. +const CUD1 = "\x1b[B" + +// CursorDown1 is a sequence for moving the cursor down one cell. +// +// This is equivalent to CursorDown(1). +// +// Deprecated: use [CUD1] instead. +const CursorDown1 = "\x1b[B" + +// CursorForward (CUF) returns a sequence for moving the cursor right n cells. +// +// # CSI n C +// +// See: https://vt100.net/docs/vt510-rm/CUF.html +func CursorForward(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "C" +} + +// CUF is an alias for [CursorForward]. +func CUF(n int) string { + return CursorForward(n) +} + +// CUF1 is a sequence for moving the cursor right one cell. +const CUF1 = "\x1b[C" + +// CursorRight (CUF) returns a sequence for moving the cursor right n cells. +// +// CSI n C +// +// See: https://vt100.net/docs/vt510-rm/CUF.html +// +// Deprecated: use [CursorForward] instead. +func CursorRight(n int) string { + return CursorForward(n) +} + +// CursorRight1 is a sequence for moving the cursor right one cell. +// +// This is equivalent to CursorRight(1). +// +// Deprecated: use [CUF1] instead. +const CursorRight1 = CUF1 + +// CursorBackward (CUB) returns a sequence for moving the cursor left n cells. +// +// # CSI n D +// +// See: https://vt100.net/docs/vt510-rm/CUB.html +func CursorBackward(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "D" +} + +// CUB is an alias for [CursorBackward]. +func CUB(n int) string { + return CursorBackward(n) +} + +// CUB1 is a sequence for moving the cursor left one cell. +const CUB1 = "\x1b[D" + +// CursorLeft (CUB) returns a sequence for moving the cursor left n cells. +// +// CSI n D +// +// See: https://vt100.net/docs/vt510-rm/CUB.html +// +// Deprecated: use [CursorBackward] instead. +func CursorLeft(n int) string { + return CursorBackward(n) +} + +// CursorLeft1 is a sequence for moving the cursor left one cell. +// +// This is equivalent to CursorLeft(1). +// +// Deprecated: use [CUB1] instead. +const CursorLeft1 = CUB1 + +// CursorNextLine (CNL) returns a sequence for moving the cursor to the +// beginning of the next line n times. +// +// CSI n E +// +// See: https://vt100.net/docs/vt510-rm/CNL.html +func CursorNextLine(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "E" +} + +// CNL is an alias for [CursorNextLine]. +func CNL(n int) string { + return CursorNextLine(n) +} + +// CursorPreviousLine (CPL) returns a sequence for moving the cursor to the +// beginning of the previous line n times. +// +// CSI n F +// +// See: https://vt100.net/docs/vt510-rm/CPL.html +func CursorPreviousLine(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "F" +} + +// CPL is an alias for [CursorPreviousLine]. +func CPL(n int) string { + return CursorPreviousLine(n) +} + +// CursorHorizontalAbsolute (CHA) returns a sequence for moving the cursor to +// the given column. +// +// Default is 1. +// +// CSI n G +// +// See: https://vt100.net/docs/vt510-rm/CHA.html +func CursorHorizontalAbsolute(col int) string { + var s string + if col > 0 { + s = strconv.Itoa(col) + } + return "\x1b[" + s + "G" +} + +// CHA is an alias for [CursorHorizontalAbsolute]. +func CHA(col int) string { + return CursorHorizontalAbsolute(col) +} + +// CursorPosition (CUP) returns a sequence for setting the cursor to the +// given row and column. +// +// Default is 1,1. +// +// CSI n ; m H +// +// See: https://vt100.net/docs/vt510-rm/CUP.html +func CursorPosition(col, row int) string { + if row <= 0 && col <= 0 { + return HomeCursorPosition + } + + var r, c string + if row > 0 { + r = strconv.Itoa(row) + } + if col > 0 { + c = strconv.Itoa(col) + } + return "\x1b[" + r + ";" + c + "H" +} + +// CUP is an alias for [CursorPosition]. +func CUP(col, row int) string { + return CursorPosition(col, row) +} + +// CursorHomePosition is a sequence for moving the cursor to the upper left +// corner of the scrolling region. This is equivalent to `CursorPosition(1, 1)`. +const CursorHomePosition = "\x1b[H" + +// SetCursorPosition (CUP) returns a sequence for setting the cursor to the +// given row and column. +// +// CSI n ; m H +// +// See: https://vt100.net/docs/vt510-rm/CUP.html +// +// Deprecated: use [CursorPosition] instead. +func SetCursorPosition(col, row int) string { + if row <= 0 && col <= 0 { + return HomeCursorPosition + } + + var r, c string + if row > 0 { + r = strconv.Itoa(row) + } + if col > 0 { + c = strconv.Itoa(col) + } + return "\x1b[" + r + ";" + c + "H" +} + +// HomeCursorPosition is a sequence for moving the cursor to the upper left +// corner of the scrolling region. This is equivalent to `SetCursorPosition(1, 1)`. +// +// Deprecated: use [CursorHomePosition] instead. +const HomeCursorPosition = CursorHomePosition + +// MoveCursor (CUP) returns a sequence for setting the cursor to the +// given row and column. +// +// CSI n ; m H +// +// See: https://vt100.net/docs/vt510-rm/CUP.html +// +// Deprecated: use [CursorPosition] instead. +func MoveCursor(col, row int) string { + return SetCursorPosition(col, row) +} + +// CursorOrigin is a sequence for moving the cursor to the upper left corner of +// the display. This is equivalent to `SetCursorPosition(1, 1)`. +// +// Deprecated: use [CursorHomePosition] instead. +const CursorOrigin = "\x1b[1;1H" + +// MoveCursorOrigin is a sequence for moving the cursor to the upper left +// corner of the display. This is equivalent to `SetCursorPosition(1, 1)`. +// +// Deprecated: use [CursorHomePosition] instead. +const MoveCursorOrigin = CursorOrigin + +// CursorHorizontalForwardTab (CHT) returns a sequence for moving the cursor to +// the next tab stop n times. +// +// Default is 1. +// +// CSI n I +// +// See: https://vt100.net/docs/vt510-rm/CHT.html +func CursorHorizontalForwardTab(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "I" +} + +// CHT is an alias for [CursorHorizontalForwardTab]. +func CHT(n int) string { + return CursorHorizontalForwardTab(n) +} + +// EraseCharacter (ECH) returns a sequence for erasing n characters and moving +// the cursor to the right. This doesn't affect other cell attributes. +// +// Default is 1. +// +// CSI n X +// +// See: https://vt100.net/docs/vt510-rm/ECH.html +func EraseCharacter(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "X" +} + +// ECH is an alias for [EraseCharacter]. +func ECH(n int) string { + return EraseCharacter(n) +} + +// CursorBackwardTab (CBT) returns a sequence for moving the cursor to the +// previous tab stop n times. +// +// Default is 1. +// +// CSI n Z +// +// See: https://vt100.net/docs/vt510-rm/CBT.html +func CursorBackwardTab(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "Z" +} + +// CBT is an alias for [CursorBackwardTab]. +func CBT(n int) string { + return CursorBackwardTab(n) +} + +// VerticalPositionAbsolute (VPA) returns a sequence for moving the cursor to +// the given row. +// +// Default is 1. +// +// CSI n d +// +// See: https://vt100.net/docs/vt510-rm/VPA.html +func VerticalPositionAbsolute(row int) string { + var s string + if row > 0 { + s = strconv.Itoa(row) + } + return "\x1b[" + s + "d" +} + +// VPA is an alias for [VerticalPositionAbsolute]. +func VPA(row int) string { + return VerticalPositionAbsolute(row) +} + +// VerticalPositionRelative (VPR) returns a sequence for moving the cursor down +// n rows relative to the current position. +// +// Default is 1. +// +// CSI n e +// +// See: https://vt100.net/docs/vt510-rm/VPR.html +func VerticalPositionRelative(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "e" +} + +// VPR is an alias for [VerticalPositionRelative]. +func VPR(n int) string { + return VerticalPositionRelative(n) +} + +// HorizontalVerticalPosition (HVP) returns a sequence for moving the cursor to +// the given row and column. +// +// Default is 1,1. +// +// CSI n ; m f +// +// This has the same effect as [CursorPosition]. +// +// See: https://vt100.net/docs/vt510-rm/HVP.html +func HorizontalVerticalPosition(col, row int) string { + var r, c string + if row > 0 { + r = strconv.Itoa(row) + } + if col > 0 { + c = strconv.Itoa(col) + } + return "\x1b[" + r + ";" + c + "f" +} + +// HVP is an alias for [HorizontalVerticalPosition]. +func HVP(col, row int) string { + return HorizontalVerticalPosition(col, row) +} + +// HorizontalVerticalHomePosition is a sequence for moving the cursor to the +// upper left corner of the scrolling region. This is equivalent to +// `HorizontalVerticalPosition(1, 1)`. +const HorizontalVerticalHomePosition = "\x1b[f" + +// SaveCurrentCursorPosition (SCOSC) is a sequence for saving the current cursor +// position for SCO console mode. +// +// CSI s +// +// This acts like [DECSC], except the page number where the cursor is located +// is not saved. +// +// See: https://vt100.net/docs/vt510-rm/SCOSC.html +const ( + SaveCurrentCursorPosition = "\x1b[s" + SCOSC = SaveCurrentCursorPosition +) + +// SaveCursorPosition (SCP or SCOSC) is a sequence for saving the cursor +// position. +// +// CSI s +// +// This acts like Save, except the page number where the cursor is located is +// not saved. +// +// See: https://vt100.net/docs/vt510-rm/SCOSC.html +// +// Deprecated: use [SaveCurrentCursorPosition] instead. +const SaveCursorPosition = "\x1b[s" + +// RestoreCurrentCursorPosition (SCORC) is a sequence for restoring the current +// cursor position for SCO console mode. +// +// CSI u +// +// This acts like [DECRC], except the page number where the cursor was saved is +// not restored. +// +// See: https://vt100.net/docs/vt510-rm/SCORC.html +const ( + RestoreCurrentCursorPosition = "\x1b[u" + SCORC = RestoreCurrentCursorPosition +) + +// RestoreCursorPosition (RCP or SCORC) is a sequence for restoring the cursor +// position. +// +// CSI u +// +// This acts like Restore, except the cursor stays on the same page where the +// cursor was saved. +// +// See: https://vt100.net/docs/vt510-rm/SCORC.html +// +// Deprecated: use [RestoreCurrentCursorPosition] instead. +const RestoreCursorPosition = "\x1b[u" + +// SetCursorStyle (DECSCUSR) returns a sequence for changing the cursor style. +// +// Default is 1. +// +// CSI Ps SP q +// +// Where Ps is the cursor style: +// +// 0: Blinking block +// 1: Blinking block (default) +// 2: Steady block +// 3: Blinking underline +// 4: Steady underline +// 5: Blinking bar (xterm) +// 6: Steady bar (xterm) +// +// See: https://vt100.net/docs/vt510-rm/DECSCUSR.html +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-Ps-SP-q.1D81 +func SetCursorStyle(style int) string { + if style < 0 { + style = 0 + } + return "\x1b[" + strconv.Itoa(style) + " q" +} + +// DECSCUSR is an alias for [SetCursorStyle]. +func DECSCUSR(style int) string { + return SetCursorStyle(style) +} + +// SetPointerShape returns a sequence for changing the mouse pointer cursor +// shape. Use "default" for the default pointer shape. +// +// OSC 22 ; Pt ST +// OSC 22 ; Pt BEL +// +// Where Pt is the pointer shape name. The name can be anything that the +// operating system can understand. Some common names are: +// +// - copy +// - crosshair +// - default +// - ew-resize +// - n-resize +// - text +// - wait +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands +func SetPointerShape(shape string) string { + return "\x1b]22;" + shape + "\x07" +} + +// ReverseIndex (RI) is an escape sequence for moving the cursor up one line in +// the same column. If the cursor is at the top margin, the screen scrolls +// down. +// +// This has the same effect as [RI]. +const ReverseIndex = "\x1bM" + +// HorizontalPositionAbsolute (HPA) returns a sequence for moving the cursor to +// the given column. This has the same effect as [CUP]. +// +// Default is 1. +// +// CSI n ` +// +// See: https://vt100.net/docs/vt510-rm/HPA.html +func HorizontalPositionAbsolute(col int) string { + var s string + if col > 0 { + s = strconv.Itoa(col) + } + return "\x1b[" + s + "`" +} + +// HPA is an alias for [HorizontalPositionAbsolute]. +func HPA(col int) string { + return HorizontalPositionAbsolute(col) +} + +// HorizontalPositionRelative (HPR) returns a sequence for moving the cursor +// right n columns relative to the current position. This has the same effect +// as [CUP]. +// +// Default is 1. +// +// CSI n a +// +// See: https://vt100.net/docs/vt510-rm/HPR.html +func HorizontalPositionRelative(n int) string { + var s string + if n > 0 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "a" +} + +// HPR is an alias for [HorizontalPositionRelative]. +func HPR(n int) string { + return HorizontalPositionRelative(n) +} + +// Index (IND) is an escape sequence for moving the cursor down one line in the +// same column. If the cursor is at the bottom margin, the screen scrolls up. +// This has the same effect as [IND]. +const Index = "\x1bD" diff --git a/vendor/github.com/charmbracelet/x/ansi/cwd.go b/vendor/github.com/charmbracelet/x/ansi/cwd.go new file mode 100644 index 0000000..b03ac1b --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/cwd.go @@ -0,0 +1,26 @@ +package ansi + +import ( + "net/url" + "path" +) + +// NotifyWorkingDirectory returns a sequence that notifies the terminal +// of the current working directory. +// +// OSC 7 ; Pt BEL +// +// Where Pt is a URL in the format "file://[host]/[path]". +// Set host to "localhost" if this is a path on the local computer. +// +// See: https://wezfurlong.org/wezterm/shell-integration.html#osc-7-escape-sequence-to-set-the-working-directory +// See: https://iterm2.com/documentation-escape-codes.html#:~:text=RemoteHost%20and%20CurrentDir%3A-,OSC%207,-%3B%20%5BPs%5D%20ST +func NotifyWorkingDirectory(host string, paths ...string) string { + path := path.Join(paths...) + u := &url.URL{ + Scheme: "file", + Host: host, + Path: path, + } + return "\x1b]7;" + u.String() + "\x07" +} diff --git a/vendor/github.com/charmbracelet/x/ansi/doc.go b/vendor/github.com/charmbracelet/x/ansi/doc.go new file mode 100644 index 0000000..e955e9f --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/doc.go @@ -0,0 +1,7 @@ +// Package ansi defines common ANSI escape sequences based on the ECMA-48 +// specs. +// +// All sequences use 7-bit C1 control codes, which are supported by most +// terminal emulators. OSC sequences are terminated by a BEL for wider +// compatibility with terminals. +package ansi diff --git a/vendor/github.com/charmbracelet/x/ansi/focus.go b/vendor/github.com/charmbracelet/x/ansi/focus.go new file mode 100644 index 0000000..4e0207c --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/focus.go @@ -0,0 +1,9 @@ +package ansi + +// Focus is an escape sequence to notify the terminal that it has focus. +// This is used with [FocusEventMode]. +const Focus = "\x1b[I" + +// Blur is an escape sequence to notify the terminal that it has lost focus. +// This is used with [FocusEventMode]. +const Blur = "\x1b[O" diff --git a/vendor/github.com/charmbracelet/x/ansi/graphics.go b/vendor/github.com/charmbracelet/x/ansi/graphics.go new file mode 100644 index 0000000..604fef4 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/graphics.go @@ -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 +} diff --git a/vendor/github.com/charmbracelet/x/ansi/hyperlink.go b/vendor/github.com/charmbracelet/x/ansi/hyperlink.go new file mode 100644 index 0000000..323bfe9 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/hyperlink.go @@ -0,0 +1,28 @@ +package ansi + +import "strings" + +// SetHyperlink returns a sequence for starting a hyperlink. +// +// OSC 8 ; Params ; Uri ST +// OSC 8 ; Params ; Uri BEL +// +// To reset the hyperlink, omit the URI. +// +// See: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda +func SetHyperlink(uri string, params ...string) string { + var p string + if len(params) > 0 { + p = strings.Join(params, ":") + } + return "\x1b]8;" + p + ";" + uri + "\x07" +} + +// ResetHyperlink returns a sequence for resetting the hyperlink. +// +// This is equivalent to SetHyperlink("", params...). +// +// See: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda +func ResetHyperlink(params ...string) string { + return SetHyperlink("", params...) +} diff --git a/vendor/github.com/charmbracelet/x/ansi/iterm2.go b/vendor/github.com/charmbracelet/x/ansi/iterm2.go new file mode 100644 index 0000000..0ecb336 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/iterm2.go @@ -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" +} diff --git a/vendor/github.com/charmbracelet/x/ansi/keypad.go b/vendor/github.com/charmbracelet/x/ansi/keypad.go new file mode 100644 index 0000000..9183c6a --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/keypad.go @@ -0,0 +1,28 @@ +package ansi + +// Keypad Application Mode (DECKPAM) is a mode that determines whether the +// keypad sends application sequences or ANSI sequences. +// +// This works like enabling [DECNKM]. +// Use [NumericKeypadMode] to set the numeric keypad mode. +// +// ESC = +// +// See: https://vt100.net/docs/vt510-rm/DECKPAM.html +const ( + KeypadApplicationMode = "\x1b=" + DECKPAM = KeypadApplicationMode +) + +// Keypad Numeric Mode (DECKPNM) is a mode that determines whether the keypad +// sends application sequences or ANSI sequences. +// +// This works the same as disabling [DECNKM]. +// +// ESC > +// +// See: https://vt100.net/docs/vt510-rm/DECKPNM.html +const ( + KeypadNumericMode = "\x1b>" + DECKPNM = KeypadNumericMode +) diff --git a/vendor/github.com/charmbracelet/x/ansi/kitty.go b/vendor/github.com/charmbracelet/x/ansi/kitty.go new file mode 100644 index 0000000..124ab83 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/kitty.go @@ -0,0 +1,90 @@ +package ansi + +import "strconv" + +// Kitty keyboard protocol progressive enhancement flags. +// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement +const ( + KittyDisambiguateEscapeCodes = 1 << iota + KittyReportEventTypes + KittyReportAlternateKeys + KittyReportAllKeysAsEscapeCodes + KittyReportAssociatedKeys + + KittyAllFlags = KittyDisambiguateEscapeCodes | KittyReportEventTypes | + KittyReportAlternateKeys | KittyReportAllKeysAsEscapeCodes | KittyReportAssociatedKeys +) + +// RequestKittyKeyboard is a sequence to request the terminal Kitty keyboard +// protocol enabled flags. +// +// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/ +const RequestKittyKeyboard = "\x1b[?u" + +// KittyKeyboard returns a sequence to request keyboard enhancements from the terminal. +// The flags argument is a bitmask of the Kitty keyboard protocol flags. While +// mode specifies how the flags should be interpreted. +// +// Possible values for flags mask: +// +// 1: Disambiguate escape codes +// 2: Report event types +// 4: Report alternate keys +// 8: Report all keys as escape codes +// 16: Report associated text +// +// Possible values for mode: +// +// 1: Set given flags and unset all others +// 2: Set given flags and keep existing flags unchanged +// 3: Unset given flags and keep existing flags unchanged +// +// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement +func KittyKeyboard(flags, mode int) string { + return "\x1b[=" + strconv.Itoa(flags) + ";" + strconv.Itoa(mode) + "u" +} + +// PushKittyKeyboard returns a sequence to push the given flags to the terminal +// Kitty Keyboard stack. +// +// Possible values for flags mask: +// +// 0: Disable all features +// 1: Disambiguate escape codes +// 2: Report event types +// 4: Report alternate keys +// 8: Report all keys as escape codes +// 16: Report associated text +// +// CSI > flags u +// +// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement +func PushKittyKeyboard(flags int) string { + var f string + if flags > 0 { + f = strconv.Itoa(flags) + } + + return "\x1b[>" + f + "u" +} + +// DisableKittyKeyboard is a sequence to push zero into the terminal Kitty +// Keyboard stack to disable the protocol. +// +// This is equivalent to PushKittyKeyboard(0). +const DisableKittyKeyboard = "\x1b[>u" + +// PopKittyKeyboard returns a sequence to pop n number of flags from the +// terminal Kitty Keyboard stack. +// +// CSI < flags u +// +// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement +func PopKittyKeyboard(n int) string { + var num string + if n > 0 { + num = strconv.Itoa(n) + } + + return "\x1b[<" + num + "u" +} diff --git a/vendor/github.com/charmbracelet/x/ansi/kitty/decoder.go b/vendor/github.com/charmbracelet/x/ansi/kitty/decoder.go new file mode 100644 index 0000000..fbd0844 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/kitty/decoder.go @@ -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 +} diff --git a/vendor/github.com/charmbracelet/x/ansi/kitty/encoder.go b/vendor/github.com/charmbracelet/x/ansi/kitty/encoder.go new file mode 100644 index 0000000..f668b9e --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/kitty/encoder.go @@ -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 +} diff --git a/vendor/github.com/charmbracelet/x/ansi/kitty/graphics.go b/vendor/github.com/charmbracelet/x/ansi/kitty/graphics.go new file mode 100644 index 0000000..490e7a8 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/kitty/graphics.go @@ -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', +} diff --git a/vendor/github.com/charmbracelet/x/ansi/kitty/options.go b/vendor/github.com/charmbracelet/x/ansi/kitty/options.go new file mode 100644 index 0000000..a8d907b --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/kitty/options.go @@ -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 +} diff --git a/vendor/github.com/charmbracelet/x/ansi/method.go b/vendor/github.com/charmbracelet/x/ansi/method.go new file mode 100644 index 0000000..0218809 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/method.go @@ -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) +} diff --git a/vendor/github.com/charmbracelet/x/ansi/mode.go b/vendor/github.com/charmbracelet/x/ansi/mode.go new file mode 100644 index 0000000..57f3f0a --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/mode.go @@ -0,0 +1,763 @@ +package ansi + +import ( + "strconv" + "strings" +) + +// ModeSetting represents a mode setting. +type ModeSetting byte + +// ModeSetting constants. +const ( + ModeNotRecognized ModeSetting = iota + ModeSet + ModeReset + ModePermanentlySet + ModePermanentlyReset +) + +// IsNotRecognized returns true if the mode is not recognized. +func (m ModeSetting) IsNotRecognized() bool { + return m == ModeNotRecognized +} + +// IsSet returns true if the mode is set or permanently set. +func (m ModeSetting) IsSet() bool { + return m == ModeSet || m == ModePermanentlySet +} + +// IsReset returns true if the mode is reset or permanently reset. +func (m ModeSetting) IsReset() bool { + return m == ModeReset || m == ModePermanentlyReset +} + +// IsPermanentlySet returns true if the mode is permanently set. +func (m ModeSetting) IsPermanentlySet() bool { + return m == ModePermanentlySet +} + +// IsPermanentlyReset returns true if the mode is permanently reset. +func (m ModeSetting) IsPermanentlyReset() bool { + return m == ModePermanentlyReset +} + +// Mode represents an interface for terminal modes. +// Modes can be set, reset, and requested. +type Mode interface { + Mode() int +} + +// 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 function will returns two escape +// sequences. +// +// ANSI format: +// +// CSI Pd ; ... ; Pd h +// +// DEC format: +// +// CSI ? Pd ; ... ; Pd h +// +// See: https://vt100.net/docs/vt510-rm/SM.html +func SetMode(modes ...Mode) string { + return setMode(false, modes...) +} + +// SM is an alias for [SetMode]. +func SM(modes ...Mode) string { + return SetMode(modes...) +} + +// 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 function will returns two escape +// sequences. +// +// ANSI format: +// +// CSI Pd ; ... ; Pd l +// +// DEC format: +// +// CSI ? Pd ; ... ; Pd l +// +// See: https://vt100.net/docs/vt510-rm/RM.html +func ResetMode(modes ...Mode) string { + return setMode(true, modes...) +} + +// RM is an alias for [ResetMode]. +func RM(modes ...Mode) string { + return ResetMode(modes...) +} + +func setMode(reset bool, modes ...Mode) (s string) { + if len(modes) == 0 { + return + } + + cmd := "h" + if reset { + cmd = "l" + } + + seq := "\x1b[" + if len(modes) == 1 { + switch modes[0].(type) { + case DECMode: + seq += "?" + } + return seq + strconv.Itoa(modes[0].Mode()) + cmd + } + + dec := make([]string, 0, len(modes)/2) + ansi := make([]string, 0, len(modes)/2) + for _, m := range modes { + switch m.(type) { + case DECMode: + dec = append(dec, strconv.Itoa(m.Mode())) + case ANSIMode: + ansi = append(ansi, strconv.Itoa(m.Mode())) + } + } + + if len(ansi) > 0 { + s += seq + strings.Join(ansi, ";") + cmd + } + if len(dec) > 0 { + s += seq + "?" + strings.Join(dec, ";") + cmd + } + return +} + +// RequestMode (DECRQM) returns a sequence to request a mode from the terminal. +// The terminal responds with a report mode function [DECRPM]. +// +// ANSI format: +// +// CSI Pa $ p +// +// DEC format: +// +// CSI ? Pa $ p +// +// See: https://vt100.net/docs/vt510-rm/DECRQM.html +func RequestMode(m Mode) string { + seq := "\x1b[" + switch m.(type) { + case DECMode: + seq += "?" + } + return seq + strconv.Itoa(m.Mode()) + "$p" +} + +// DECRQM is an alias for [RequestMode]. +func DECRQM(m Mode) string { + return RequestMode(m) +} + +// ReportMode (DECRPM) returns a sequence that the terminal sends to the host +// in response to a mode request [DECRQM]. +// +// ANSI format: +// +// CSI Pa ; Ps ; $ y +// +// DEC format: +// +// CSI ? Pa ; Ps $ y +// +// Where Pa is the mode number, and Ps is the mode value. +// +// 0: Not recognized +// 1: Set +// 2: Reset +// 3: Permanent set +// 4: Permanent reset +// +// See: https://vt100.net/docs/vt510-rm/DECRPM.html +func ReportMode(mode Mode, value ModeSetting) string { + if value > 4 { + value = 0 + } + switch mode.(type) { + case DECMode: + return "\x1b[?" + strconv.Itoa(mode.Mode()) + ";" + strconv.Itoa(int(value)) + "$y" + } + return "\x1b[" + strconv.Itoa(mode.Mode()) + ";" + strconv.Itoa(int(value)) + "$y" +} + +// DECRPM is an alias for [ReportMode]. +func DECRPM(mode Mode, value ModeSetting) string { + return ReportMode(mode, value) +} + +// ANSIMode represents an ANSI terminal mode. +type ANSIMode int //nolint:revive + +// Mode returns the ANSI mode as an integer. +func (m ANSIMode) Mode() int { + return int(m) +} + +// DECMode represents a private DEC terminal mode. +type DECMode int + +// Mode returns the DEC mode as an integer. +func (m DECMode) Mode() int { + return int(m) +} + +// Keyboard Action Mode (KAM) is a mode that controls locking of the keyboard. +// When the keyboard is locked, it cannot send data to the terminal. +// +// See: https://vt100.net/docs/vt510-rm/KAM.html +const ( + KeyboardActionMode = ANSIMode(2) + KAM = KeyboardActionMode + + SetKeyboardActionMode = "\x1b[2h" + ResetKeyboardActionMode = "\x1b[2l" + RequestKeyboardActionMode = "\x1b[2$p" +) + +// Insert/Replace Mode (IRM) is a mode that determines whether characters are +// inserted or replaced when typed. +// +// When enabled, characters are inserted at the cursor position pushing the +// characters to the right. When disabled, characters replace the character at +// the cursor position. +// +// See: https://vt100.net/docs/vt510-rm/IRM.html +const ( + InsertReplaceMode = ANSIMode(4) + IRM = InsertReplaceMode + + SetInsertReplaceMode = "\x1b[4h" + ResetInsertReplaceMode = "\x1b[4l" + RequestInsertReplaceMode = "\x1b[4$p" +) + +// Send Receive Mode (SRM) or Local Echo Mode is a mode that determines whether +// the terminal echoes characters back to the host. When enabled, the terminal +// sends characters to the host as they are typed. +// +// See: https://vt100.net/docs/vt510-rm/SRM.html +const ( + SendReceiveMode = ANSIMode(12) + LocalEchoMode = SendReceiveMode + SRM = SendReceiveMode + + SetSendReceiveMode = "\x1b[12h" + ResetSendReceiveMode = "\x1b[12l" + RequestSendReceiveMode = "\x1b[12$p" + + SetLocalEchoMode = "\x1b[12h" + ResetLocalEchoMode = "\x1b[12l" + RequestLocalEchoMode = "\x1b[12$p" +) + +// Line Feed/New Line Mode (LNM) is a mode that determines whether the terminal +// interprets the line feed character as a new line. +// +// When enabled, the terminal interprets the line feed character as a new line. +// When disabled, the terminal interprets the line feed character as a line feed. +// +// A new line moves the cursor to the first position of the next line. +// A line feed moves the cursor down one line without changing the column +// scrolling the screen if necessary. +// +// See: https://vt100.net/docs/vt510-rm/LNM.html +const ( + LineFeedNewLineMode = ANSIMode(20) + LNM = LineFeedNewLineMode + + SetLineFeedNewLineMode = "\x1b[20h" + ResetLineFeedNewLineMode = "\x1b[20l" + RequestLineFeedNewLineMode = "\x1b[20$p" +) + +// Cursor Keys Mode (DECCKM) is a mode that determines whether the cursor keys +// send ANSI cursor sequences or application sequences. +// +// See: https://vt100.net/docs/vt510-rm/DECCKM.html +const ( + CursorKeysMode = DECMode(1) + DECCKM = CursorKeysMode + + SetCursorKeysMode = "\x1b[?1h" + ResetCursorKeysMode = "\x1b[?1l" + RequestCursorKeysMode = "\x1b[?1$p" +) + +// Deprecated: use [SetCursorKeysMode] and [ResetCursorKeysMode] instead. +const ( + EnableCursorKeys = "\x1b[?1h" + DisableCursorKeys = "\x1b[?1l" +) + +// Origin Mode (DECOM) is a mode that determines whether the cursor moves to the +// home position or the margin position. +// +// See: https://vt100.net/docs/vt510-rm/DECOM.html +const ( + OriginMode = DECMode(6) + DECOM = OriginMode + + SetOriginMode = "\x1b[?6h" + ResetOriginMode = "\x1b[?6l" + RequestOriginMode = "\x1b[?6$p" +) + +// Auto Wrap Mode (DECAWM) is a mode that determines whether the cursor wraps +// to the next line when it reaches the right margin. +// +// See: https://vt100.net/docs/vt510-rm/DECAWM.html +const ( + AutoWrapMode = DECMode(7) + DECAWM = AutoWrapMode + + SetAutoWrapMode = "\x1b[?7h" + ResetAutoWrapMode = "\x1b[?7l" + RequestAutoWrapMode = "\x1b[?7$p" +) + +// X10 Mouse Mode is a mode that determines whether the mouse reports on button +// presses. +// +// The terminal responds with the following encoding: +// +// CSI M CbCxCy +// +// Where Cb is the button-1, where it can be 1, 2, or 3. +// Cx and Cy are the x and y coordinates of the mouse event. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +const ( + X10MouseMode = DECMode(9) + + SetX10MouseMode = "\x1b[?9h" + ResetX10MouseMode = "\x1b[?9l" + RequestX10MouseMode = "\x1b[?9$p" +) + +// Text Cursor Enable Mode (DECTCEM) is a mode that shows/hides the cursor. +// +// See: https://vt100.net/docs/vt510-rm/DECTCEM.html +const ( + TextCursorEnableMode = DECMode(25) + DECTCEM = TextCursorEnableMode + + SetTextCursorEnableMode = "\x1b[?25h" + ResetTextCursorEnableMode = "\x1b[?25l" + RequestTextCursorEnableMode = "\x1b[?25$p" +) + +// These are aliases for [SetTextCursorEnableMode] and [ResetTextCursorEnableMode]. +const ( + ShowCursor = SetTextCursorEnableMode + HideCursor = ResetTextCursorEnableMode +) + +// Text Cursor Enable Mode (DECTCEM) is a mode that shows/hides the cursor. +// +// See: https://vt100.net/docs/vt510-rm/DECTCEM.html +// +// Deprecated: use [SetTextCursorEnableMode] and [ResetTextCursorEnableMode] instead. +const ( + CursorEnableMode = DECMode(25) + RequestCursorVisibility = "\x1b[?25$p" +) + +// Numeric Keypad Mode (DECNKM) is a mode that determines whether the keypad +// sends application sequences or numeric sequences. +// +// This works like [DECKPAM] and [DECKPNM], but uses different sequences. +// +// See: https://vt100.net/docs/vt510-rm/DECNKM.html +const ( + NumericKeypadMode = DECMode(66) + DECNKM = NumericKeypadMode + + SetNumericKeypadMode = "\x1b[?66h" + ResetNumericKeypadMode = "\x1b[?66l" + RequestNumericKeypadMode = "\x1b[?66$p" +) + +// Backarrow Key Mode (DECBKM) is a mode that determines whether the backspace +// key sends a backspace or delete character. Disabled by default. +// +// See: https://vt100.net/docs/vt510-rm/DECBKM.html +const ( + BackarrowKeyMode = DECMode(67) + DECBKM = BackarrowKeyMode + + SetBackarrowKeyMode = "\x1b[?67h" + ResetBackarrowKeyMode = "\x1b[?67l" + RequestBackarrowKeyMode = "\x1b[?67$p" +) + +// Left Right Margin Mode (DECLRMM) is a mode that determines whether the left +// and right margins can be set with [DECSLRM]. +// +// See: https://vt100.net/docs/vt510-rm/DECLRMM.html +const ( + LeftRightMarginMode = DECMode(69) + DECLRMM = LeftRightMarginMode + + SetLeftRightMarginMode = "\x1b[?69h" + ResetLeftRightMarginMode = "\x1b[?69l" + RequestLeftRightMarginMode = "\x1b[?69$p" +) + +// Normal Mouse Mode is a mode that determines whether the mouse reports on +// button presses and releases. It will also report modifier keys, wheel +// events, and extra buttons. +// +// It uses the same encoding as [X10MouseMode] with a few differences: +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +const ( + NormalMouseMode = DECMode(1000) + + SetNormalMouseMode = "\x1b[?1000h" + ResetNormalMouseMode = "\x1b[?1000l" + RequestNormalMouseMode = "\x1b[?1000$p" +) + +// VT Mouse Tracking is a mode that determines whether the mouse reports on +// button press and release. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +// +// Deprecated: use [NormalMouseMode] instead. +const ( + MouseMode = DECMode(1000) + + EnableMouse = "\x1b[?1000h" + DisableMouse = "\x1b[?1000l" + RequestMouse = "\x1b[?1000$p" +) + +// Highlight Mouse Tracking is a mode that determines whether the mouse reports +// on button presses, releases, and highlighted cells. +// +// It uses the same encoding as [NormalMouseMode] with a few differences: +// +// On highlight events, the terminal responds with the following encoding: +// +// CSI t CxCy +// CSI T CxCyCxCyCxCy +// +// Where the parameters are startx, starty, endx, endy, mousex, and mousey. +const ( + HighlightMouseMode = DECMode(1001) + + SetHighlightMouseMode = "\x1b[?1001h" + ResetHighlightMouseMode = "\x1b[?1001l" + RequestHighlightMouseMode = "\x1b[?1001$p" +) + +// VT Hilite Mouse Tracking is a mode that determines whether the mouse reports on +// button presses, releases, and highlighted cells. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +// +// Deprecated: use [HighlightMouseMode] instead. +const ( + MouseHiliteMode = DECMode(1001) + + EnableMouseHilite = "\x1b[?1001h" + DisableMouseHilite = "\x1b[?1001l" + RequestMouseHilite = "\x1b[?1001$p" +) + +// Button Event Mouse Tracking is essentially the same as [NormalMouseMode], +// but it also reports button-motion events when a button is pressed. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +const ( + ButtonEventMouseMode = DECMode(1002) + + SetButtonEventMouseMode = "\x1b[?1002h" + ResetButtonEventMouseMode = "\x1b[?1002l" + RequestButtonEventMouseMode = "\x1b[?1002$p" +) + +// Cell Motion Mouse Tracking is a mode that determines whether the mouse +// reports on button press, release, and motion events. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +// +// Deprecated: use [ButtonEventMouseMode] instead. +const ( + MouseCellMotionMode = DECMode(1002) + + EnableMouseCellMotion = "\x1b[?1002h" + DisableMouseCellMotion = "\x1b[?1002l" + RequestMouseCellMotion = "\x1b[?1002$p" +) + +// Any Event Mouse Tracking is the same as [ButtonEventMouseMode], except that +// all motion events are reported even if no mouse buttons are pressed. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +const ( + AnyEventMouseMode = DECMode(1003) + + SetAnyEventMouseMode = "\x1b[?1003h" + ResetAnyEventMouseMode = "\x1b[?1003l" + RequestAnyEventMouseMode = "\x1b[?1003$p" +) + +// All Mouse Tracking is a mode that determines whether the mouse reports on +// button press, release, motion, and highlight events. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +// +// Deprecated: use [AnyEventMouseMode] instead. +const ( + MouseAllMotionMode = DECMode(1003) + + EnableMouseAllMotion = "\x1b[?1003h" + DisableMouseAllMotion = "\x1b[?1003l" + RequestMouseAllMotion = "\x1b[?1003$p" +) + +// Focus Event Mode is a mode that determines whether the terminal reports focus +// and blur events. +// +// The terminal sends the following encoding: +// +// CSI I // Focus In +// CSI O // Focus Out +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Focus-Tracking +const ( + FocusEventMode = DECMode(1004) + + SetFocusEventMode = "\x1b[?1004h" + ResetFocusEventMode = "\x1b[?1004l" + RequestFocusEventMode = "\x1b[?1004$p" +) + +// Deprecated: use [SetFocusEventMode], [ResetFocusEventMode], and +// [RequestFocusEventMode] instead. +const ( + ReportFocusMode = DECMode(1004) + + EnableReportFocus = "\x1b[?1004h" + DisableReportFocus = "\x1b[?1004l" + RequestReportFocus = "\x1b[?1004$p" +) + +// SGR Extended Mouse Mode is a mode that changes the mouse tracking encoding +// to use SGR parameters. +// +// The terminal responds with the following encoding: +// +// CSI < Cb ; Cx ; Cy M +// +// Where Cb is the same as [NormalMouseMode], and Cx and Cy are the x and y. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +const ( + SgrExtMouseMode = DECMode(1006) + + SetSgrExtMouseMode = "\x1b[?1006h" + ResetSgrExtMouseMode = "\x1b[?1006l" + RequestSgrExtMouseMode = "\x1b[?1006$p" +) + +// Deprecated: use [SgrExtMouseMode] [SetSgrExtMouseMode], +// [ResetSgrExtMouseMode], and [RequestSgrExtMouseMode] instead. +const ( + MouseSgrExtMode = DECMode(1006) + EnableMouseSgrExt = "\x1b[?1006h" + DisableMouseSgrExt = "\x1b[?1006l" + RequestMouseSgrExt = "\x1b[?1006$p" +) + +// UTF-8 Extended Mouse Mode is a mode that changes the mouse tracking encoding +// to use UTF-8 parameters. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +const ( + Utf8ExtMouseMode = DECMode(1005) + + SetUtf8ExtMouseMode = "\x1b[?1005h" + ResetUtf8ExtMouseMode = "\x1b[?1005l" + RequestUtf8ExtMouseMode = "\x1b[?1005$p" +) + +// URXVT Extended Mouse Mode is a mode that changes the mouse tracking encoding +// to use an alternate encoding. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +const ( + UrxvtExtMouseMode = DECMode(1015) + + SetUrxvtExtMouseMode = "\x1b[?1015h" + ResetUrxvtExtMouseMode = "\x1b[?1015l" + RequestUrxvtExtMouseMode = "\x1b[?1015$p" +) + +// SGR Pixel Extended Mouse Mode is a mode that changes the mouse tracking +// encoding to use SGR parameters with pixel coordinates. +// +// This is similar to [SgrExtMouseMode], but also reports pixel coordinates. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +const ( + SgrPixelExtMouseMode = DECMode(1016) + + SetSgrPixelExtMouseMode = "\x1b[?1016h" + ResetSgrPixelExtMouseMode = "\x1b[?1016l" + RequestSgrPixelExtMouseMode = "\x1b[?1016$p" +) + +// Alternate Screen Mode is a mode that determines whether the alternate screen +// buffer is active. When this mode is enabled, the alternate screen buffer is +// cleared. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer +const ( + AltScreenMode = DECMode(1047) + + SetAltScreenMode = "\x1b[?1047h" + ResetAltScreenMode = "\x1b[?1047l" + RequestAltScreenMode = "\x1b[?1047$p" +) + +// Save Cursor Mode is a mode that saves the cursor position. +// This is equivalent to [SaveCursor] and [RestoreCursor]. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer +const ( + SaveCursorMode = DECMode(1048) + + SetSaveCursorMode = "\x1b[?1048h" + ResetSaveCursorMode = "\x1b[?1048l" + RequestSaveCursorMode = "\x1b[?1048$p" +) + +// Alternate Screen Save Cursor Mode is a mode that saves the cursor position as in +// [SaveCursorMode], switches to the alternate screen buffer as in [AltScreenMode], +// and clears the screen on switch. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer +const ( + AltScreenSaveCursorMode = DECMode(1049) + + SetAltScreenSaveCursorMode = "\x1b[?1049h" + ResetAltScreenSaveCursorMode = "\x1b[?1049l" + RequestAltScreenSaveCursorMode = "\x1b[?1049$p" +) + +// Alternate Screen Buffer is a mode that determines whether the alternate screen +// buffer is active. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer +// +// Deprecated: use [AltScreenSaveCursorMode] instead. +const ( + AltScreenBufferMode = DECMode(1049) + + SetAltScreenBufferMode = "\x1b[?1049h" + ResetAltScreenBufferMode = "\x1b[?1049l" + RequestAltScreenBufferMode = "\x1b[?1049$p" + + EnableAltScreenBuffer = "\x1b[?1049h" + DisableAltScreenBuffer = "\x1b[?1049l" + RequestAltScreenBuffer = "\x1b[?1049$p" +) + +// Bracketed Paste Mode is a mode that determines whether pasted text is +// bracketed with escape sequences. +// +// See: https://cirw.in/blog/bracketed-paste +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Bracketed-Paste-Mode +const ( + BracketedPasteMode = DECMode(2004) + + SetBracketedPasteMode = "\x1b[?2004h" + ResetBracketedPasteMode = "\x1b[?2004l" + RequestBracketedPasteMode = "\x1b[?2004$p" +) + +// Deprecated: use [SetBracketedPasteMode], [ResetBracketedPasteMode], and +// [RequestBracketedPasteMode] instead. +const ( + EnableBracketedPaste = "\x1b[?2004h" + DisableBracketedPaste = "\x1b[?2004l" + RequestBracketedPaste = "\x1b[?2004$p" +) + +// Synchronized Output Mode is a mode that determines whether output is +// synchronized with the terminal. +// +// See: https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036 +const ( + SynchronizedOutputMode = DECMode(2026) + + SetSynchronizedOutputMode = "\x1b[?2026h" + ResetSynchronizedOutputMode = "\x1b[?2026l" + RequestSynchronizedOutputMode = "\x1b[?2026$p" +) + +// Deprecated: use [SynchronizedOutputMode], [SetSynchronizedOutputMode], and +// [ResetSynchronizedOutputMode], and [RequestSynchronizedOutputMode] instead. +const ( + SyncdOutputMode = DECMode(2026) + + EnableSyncdOutput = "\x1b[?2026h" + DisableSyncdOutput = "\x1b[?2026l" + RequestSyncdOutput = "\x1b[?2026$p" +) + +// Grapheme Clustering Mode is a mode that determines whether the terminal +// should look for grapheme clusters instead of single runes in the rendered +// text. This makes the terminal properly render combining characters such as +// emojis. +// +// See: https://github.com/contour-terminal/terminal-unicode-core +const ( + GraphemeClusteringMode = DECMode(2027) + + SetGraphemeClusteringMode = "\x1b[?2027h" + ResetGraphemeClusteringMode = "\x1b[?2027l" + RequestGraphemeClusteringMode = "\x1b[?2027$p" +) + +// Deprecated: use [SetGraphemeClusteringMode], [ResetGraphemeClusteringMode], and +// [RequestGraphemeClusteringMode] instead. +const ( + EnableGraphemeClustering = "\x1b[?2027h" + DisableGraphemeClustering = "\x1b[?2027l" + RequestGraphemeClustering = "\x1b[?2027$p" +) + +// Win32Input is a mode that determines whether input is processed by the +// Win32 console and Conpty. +// +// See: https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md +const ( + Win32InputMode = DECMode(9001) + + SetWin32InputMode = "\x1b[?9001h" + ResetWin32InputMode = "\x1b[?9001l" + RequestWin32InputMode = "\x1b[?9001$p" +) + +// Deprecated: use [SetWin32InputMode], [ResetWin32InputMode], and +// [RequestWin32InputMode] instead. +const ( + EnableWin32Input = "\x1b[?9001h" + DisableWin32Input = "\x1b[?9001l" + RequestWin32Input = "\x1b[?9001$p" +) diff --git a/vendor/github.com/charmbracelet/x/ansi/modes.go b/vendor/github.com/charmbracelet/x/ansi/modes.go new file mode 100644 index 0000000..1bec5bc --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/modes.go @@ -0,0 +1,71 @@ +package ansi + +// Modes represents the terminal modes that can be set or reset. By default, +// all modes are [ModeNotRecognized]. +type Modes map[Mode]ModeSetting + +// NewModes creates a new Modes map. By default, all modes are +// [ModeNotRecognized]. +func NewModes() Modes { + return make(Modes) +} + +// Get returns the setting of a terminal mode. If the mode is not set, it +// returns [ModeNotRecognized]. +func (m Modes) Get(mode Mode) ModeSetting { + return m[mode] +} + +// Delete deletes a terminal mode. This has the same effect as setting the mode +// to [ModeNotRecognized]. +func (m Modes) Delete(mode Mode) { + delete(m, mode) +} + +// Set sets a terminal mode to [ModeSet]. +func (m Modes) Set(modes ...Mode) { + for _, mode := range modes { + m[mode] = ModeSet + } +} + +// PermanentlySet sets a terminal mode to [ModePermanentlySet]. +func (m Modes) PermanentlySet(modes ...Mode) { + for _, mode := range modes { + m[mode] = ModePermanentlySet + } +} + +// Reset sets a terminal mode to [ModeReset]. +func (m Modes) Reset(modes ...Mode) { + for _, mode := range modes { + m[mode] = ModeReset + } +} + +// PermanentlyReset sets a terminal mode to [ModePermanentlyReset]. +func (m Modes) PermanentlyReset(modes ...Mode) { + for _, mode := range modes { + m[mode] = ModePermanentlyReset + } +} + +// IsSet returns true if the mode is set to [ModeSet] or [ModePermanentlySet]. +func (m Modes) IsSet(mode Mode) bool { + return m[mode].IsSet() +} + +// IsPermanentlySet returns true if the mode is set to [ModePermanentlySet]. +func (m Modes) IsPermanentlySet(mode Mode) bool { + return m[mode].IsPermanentlySet() +} + +// IsReset returns true if the mode is set to [ModeReset] or [ModePermanentlyReset]. +func (m Modes) IsReset(mode Mode) bool { + return m[mode].IsReset() +} + +// IsPermanentlyReset returns true if the mode is set to [ModePermanentlyReset]. +func (m Modes) IsPermanentlyReset(mode Mode) bool { + return m[mode].IsPermanentlyReset() +} diff --git a/vendor/github.com/charmbracelet/x/ansi/mouse.go b/vendor/github.com/charmbracelet/x/ansi/mouse.go new file mode 100644 index 0000000..95b0127 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/mouse.go @@ -0,0 +1,172 @@ +package ansi + +import ( + "fmt" +) + +// MouseButton represents the button that was pressed during a mouse message. +type MouseButton byte + +// Mouse event buttons +// +// This is based on X11 mouse button codes. +// +// 1 = left button +// 2 = middle button (pressing the scroll wheel) +// 3 = right button +// 4 = turn scroll wheel up +// 5 = turn scroll wheel down +// 6 = push scroll wheel left +// 7 = push scroll wheel right +// 8 = 4th button (aka browser backward button) +// 9 = 5th button (aka browser forward button) +// 10 +// 11 +// +// Other buttons are not supported. +const ( + MouseNone MouseButton = iota + MouseButton1 + MouseButton2 + MouseButton3 + MouseButton4 + MouseButton5 + MouseButton6 + MouseButton7 + MouseButton8 + MouseButton9 + MouseButton10 + MouseButton11 + + 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{ + MouseNone: "none", + MouseLeft: "left", + MouseMiddle: "middle", + MouseRight: "right", + MouseWheelUp: "wheelup", + MouseWheelDown: "wheeldown", + MouseWheelLeft: "wheelleft", + MouseWheelRight: "wheelright", + MouseBackward: "backward", + MouseForward: "forward", + MouseButton10: "button10", + MouseButton11: "button11", +} + +// String returns a string representation of the mouse button. +func (b MouseButton) String() string { + return mouseButtons[b] +} + +// 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: +// 0 = left button, wheel up, or button no. 8 aka (backwards) +// 1 = middle button, wheel down, or button no. 9 aka (forwards) +// 2 = right button, wheel left, or button no. 10 +// 3 = release event, wheel right, or button no. 11 +// +// - The third bit indicates whether the shift key was pressed. +// +// - The fourth bit indicates the alt key was pressed. +// +// - The fifth bit indicates the control key was pressed. +// +// - The sixth bit indicates motion events. Combined with button number 3, i.e. +// release event, it represents a drag event. +// +// - The seventh bit indicates a wheel event. +// +// - The eighth bit indicates additional buttons. +// +// If button is [MouseNone], and motion is false, this returns a release event. +// If button is undefined, this function returns 0xff. +func EncodeMouseButton(b MouseButton, motion, shift, alt, ctrl bool) (m byte) { + // mouse bit shifts + const ( + bitShift = 0b0000_0100 + bitAlt = 0b0000_1000 + bitCtrl = 0b0001_0000 + bitMotion = 0b0010_0000 + bitWheel = 0b0100_0000 + bitAdd = 0b1000_0000 // additional buttons 8-11 + + bitsMask = 0b0000_0011 + ) + + if b == MouseNone { + m = bitsMask + } else if b >= MouseLeft && b <= MouseRight { + m = byte(b - MouseLeft) + } else if b >= MouseWheelUp && b <= MouseWheelRight { + m = byte(b - MouseWheelUp) + m |= bitWheel + } else if b >= MouseBackward && b <= MouseButton11 { + m = byte(b - MouseBackward) + m |= bitAdd + } else { + m = 0xff // invalid button + } + + if shift { + m |= bitShift + } + if alt { + m |= bitAlt + } + if ctrl { + m |= bitCtrl + } + if motion { + m |= bitMotion + } + + return +} + +// x10Offset is the offset for X10 mouse events. +// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking +const x10Offset = 32 + +// MouseX10 returns an escape sequence representing a mouse event in X10 mode. +// Note that this requires the terminal support X10 mouse modes. +// +// CSI M Cb Cx Cy +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking +func MouseX10(b byte, x, y int) string { + return "\x1b[M" + string(b+x10Offset) + string(byte(x)+x10Offset+1) + string(byte(y)+x10Offset+1) +} + +// MouseSgr returns an escape sequence representing a mouse event in SGR mode. +// +// CSI < Cb ; Cx ; Cy M +// CSI < Cb ; Cx ; Cy m (release) +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking +func MouseSgr(b byte, x, y int, release bool) string { + s := 'M' + if release { + s = 'm' + } + if x < 0 { + x = -x + } + if y < 0 { + y = -y + } + return fmt.Sprintf("\x1b[<%d;%d;%d%c", b, x+1, y+1, s) +} diff --git a/vendor/github.com/charmbracelet/x/ansi/notification.go b/vendor/github.com/charmbracelet/x/ansi/notification.go new file mode 100644 index 0000000..c712f34 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/notification.go @@ -0,0 +1,13 @@ +package ansi + +// Notify sends a desktop notification using iTerm's OSC 9. +// +// OSC 9 ; Mc ST +// OSC 9 ; Mc BEL +// +// Where Mc is the notification body. +// +// See: https://iterm2.com/documentation-escape-codes.html +func Notify(s string) string { + return "\x1b]9;" + s + "\x07" +} diff --git a/vendor/github.com/charmbracelet/x/ansi/parser.go b/vendor/github.com/charmbracelet/x/ansi/parser.go new file mode 100644 index 0000000..882e1ed --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/parser.go @@ -0,0 +1,417 @@ +package ansi + +import ( + "unicode/utf8" + "unsafe" + + "github.com/charmbracelet/x/ansi/parser" +) + +// Parser represents a DEC ANSI compatible sequence parser. +// +// It uses a state machine to parse ANSI escape sequences and control +// characters. The parser is designed to be used with a terminal emulator or +// similar application that needs to parse ANSI escape sequences and control +// characters. +// See package [parser] for more information. +// +//go:generate go run ./gen.go +type Parser struct { + handler Handler + + // params contains the raw parameters of the sequence. + // These parameters used when constructing CSI and DCS sequences. + params []int + + // data contains the raw data of the sequence. + // These data used when constructing OSC, DCS, SOS, PM, and APC sequences. + data []byte + + // dataLen keeps track of the length of the data buffer. + // If dataLen is -1, the data buffer is unlimited and will grow as needed. + // Otherwise, dataLen is limited by the size of the data buffer. + dataLen int + + // paramsLen keeps track of the number of parameters. + // This is limited by the size of the params buffer. + // + // This is also used when collecting UTF-8 runes to keep track of the + // number of rune bytes collected. + paramsLen int + + // 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 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. + cmd int + + // state is the current state of the parser. + state byte +} + +// 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() *Parser { + p := new(Parser) + p.SetParamsSize(parser.MaxParamsSize) + p.SetDataSize(1024 * 64) // 64KB data buffer + return p +} + +// SetParamsSize sets the size of the parameters buffer. +// This is used when constructing CSI and DCS sequences. +func (p *Parser) SetParamsSize(size int) { + p.params = make([]int, size) +} + +// SetDataSize sets the size of the data buffer. +// This is used when constructing OSC, DCS, SOS, PM, and APC sequences. +// If size is less than or equal to 0, the data buffer is unlimited and will +// grow as needed. +func (p *Parser) SetDataSize(size int) { + if size <= 0 { + size = 0 + p.dataLen = -1 + } + p.data = make([]byte, size) +} + +// Params returns the list of parsed packed parameters. +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 +// value if the parameter is missing. If the index is out of bounds, it returns +// the default value and false. +func (p *Parser) Param(i, def int) (int, bool) { + if i < 0 || i >= p.paramsLen { + return def, false + } + return Param(p.params[i]).Param(def), true +} + +// 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. +func (p *Parser) Rune() rune { + rw := utf8ByteLen(byte(p.cmd & 0xff)) + if rw == -1 { + return utf8.RuneError + } + r, _ := utf8.DecodeRune((*[utf8.UTFMax]byte)(unsafe.Pointer(&p.cmd))[:rw]) + 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] +} + +// Reset resets the parser to its initial state. +func (p *Parser) Reset() { + p.clear() + p.state = parser.GroundState +} + +// clear clears the parser parameters and command. +func (p *Parser) clear() { + if len(p.params) > 0 { + p.params[0] = parser.MissingParam + } + p.paramsLen = 0 + p.cmd = 0 +} + +// State returns the current state of the parser. +func (p *Parser) State() parser.State { + return p.state +} + +// StateName returns the name of the current state. +func (p *Parser) StateName() string { + return parser.StateNames[p.state] +} + +// Parse parses the given dispatcher and byte buffer. +// Deprecated: Loop over the buffer and call [Parser.Advance] instead. +func (p *Parser) Parse(b []byte) { + for i := 0; i < len(b); i++ { + p.Advance(b[i]) + } +} + +// Advance advances the parser using the given byte. It returns the action +// performed by the parser. +func (p *Parser) Advance(b byte) parser.Action { + switch p.state { + case parser.Utf8State: + // We handle UTF-8 here. + return p.advanceUtf8(b) + default: + return p.advance(b) + } +} + +func (p *Parser) collectRune(b byte) { + if p.paramsLen >= utf8.UTFMax { + return + } + + shift := p.paramsLen * 8 + p.cmd &^= 0xff << shift + p.cmd |= int(b) << shift + p.paramsLen++ +} + +func (p *Parser) advanceUtf8(b byte) parser.Action { + // Collect UTF-8 rune bytes. + p.collectRune(b) + rw := utf8ByteLen(byte(p.cmd & 0xff)) + if rw == -1 { + // We panic here because the first byte comes from the state machine, + // if this panics, it means there is a bug in the state machine! + panic("invalid rune") // unreachable + } + + if p.paramsLen < rw { + return parser.CollectAction + } + + // We have enough bytes to decode the rune using unsafe + if p.handler.Print != nil { + p.handler.Print(p.Rune()) + } + + p.state = parser.GroundState + p.paramsLen = 0 + + return parser.PrintAction +} + +func (p *Parser) advance(b byte) parser.Action { + state, action := parser.Table.Transition(p.state, b) + + // We need to clear the parser state if the state changes from EscapeState. + // This is because when we enter the EscapeState, we don't get a chance to + // clear the parser state. For example, when a sequence terminates with a + // ST (\x1b\\ or \x9c), we dispatch the current sequence and transition to + // EscapeState. However, the parser state is not cleared in this case and + // we need to clear it here before dispatching the esc sequence. + if p.state != state { + if p.state == parser.EscapeState { + p.performAction(parser.ClearAction, state, b) + } + if action == parser.PutAction && + p.state == parser.DcsEntryState && state == parser.DcsStringState { + // XXX: This is a special case where we need to start collecting + // non-string parameterized data i.e. doesn't follow the ECMA-48 § + // 5.4.1 string parameters format. + p.performAction(parser.StartAction, state, 0) + } + } + + // Handle special cases + switch { + case b == ESC && p.state == parser.EscapeState: + // Two ESCs in a row + p.performAction(parser.ExecuteAction, state, b) + default: + p.performAction(action, state, b) + } + + p.state = state + + return action +} + +func (p *Parser) parseStringCmd() { + // Try to parse the command + datalen := len(p.data) + if p.dataLen >= 0 { + datalen = p.dataLen + } + for i := 0; i < datalen; i++ { + d := p.data[i] + if d < '0' || d > '9' { + break + } + if p.cmd == parser.MissingCommand { + p.cmd = 0 + } + p.cmd *= 10 + p.cmd += int(d - '0') + } +} + +func (p *Parser) performAction(action parser.Action, state parser.State, b byte) { + switch action { + case parser.IgnoreAction: + break + + case parser.ClearAction: + p.clear() + + case parser.PrintAction: + p.cmd = int(b) + if p.handler.Print != nil { + p.handler.Print(rune(b)) + } + + case parser.ExecuteAction: + p.cmd = int(b) + if p.handler.Execute != nil { + p.handler.Execute(b) + } + + 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 { + // Reset the UTF-8 counter + p.paramsLen = 0 + p.collectRune(b) + } else { + // Collect intermediate bytes + // we only store the last intermediate byte + p.cmd &^= 0xff << parser.IntermedShift + p.cmd |= int(b) << parser.IntermedShift + } + + case parser.ParamAction: + // Collect parameters + if p.paramsLen >= len(p.params) { + break + } + + if b >= '0' && b <= '9' { + if p.params[p.paramsLen] == parser.MissingParam { + p.params[p.paramsLen] = 0 + } + + p.params[p.paramsLen] *= 10 + p.params[p.paramsLen] += int(b - '0') + } + + if b == ':' { + p.params[p.paramsLen] |= parser.HasMoreFlag + } + + if b == ';' || b == ':' { + p.paramsLen++ + if p.paramsLen < len(p.params) { + p.params[p.paramsLen] = parser.MissingParam + } + } + + case parser.StartAction: + if p.dataLen < 0 && p.data != nil { + p.data = p.data[:0] + } else { + p.dataLen = 0 + } + if p.state >= parser.DcsEntryState && p.state <= parser.DcsStringState { + // Collect the command byte for DCS + p.cmd |= int(b) + } else { + p.cmd = parser.MissingCommand + } + + case parser.PutAction: + switch p.state { + case parser.OscStringState: + if b == ';' && p.cmd == parser.MissingCommand { + p.parseStringCmd() + } + } + + if p.dataLen < 0 { + p.data = append(p.data, b) + } else { + if p.dataLen < len(p.data) { + p.data[p.dataLen] = b + p.dataLen++ + } + } + + case parser.DispatchAction: + // Increment the last parameter + if p.paramsLen > 0 && p.paramsLen < len(p.params)-1 || + p.paramsLen == 0 && len(p.params) > 0 && p.params[0] != parser.MissingParam { + p.paramsLen++ + } + + if p.state == parser.OscStringState && p.cmd == parser.MissingCommand { + // Ensure we have a command for OSC + p.parseStringCmd() + } + + data := p.data + if p.dataLen >= 0 { + data = data[:p.dataLen] + } + switch p.state { + case parser.CsiEntryState, parser.CsiParamState, parser.CsiIntermediateState: + p.cmd |= int(b) + if p.handler.HandleCsi != nil { + p.handler.HandleCsi(Cmd(p.cmd), p.Params()) + } + case parser.EscapeState, parser.EscapeIntermediateState: + p.cmd |= int(b) + if p.handler.HandleEsc != nil { + p.handler.HandleEsc(Cmd(p.cmd)) + } + case parser.DcsEntryState, parser.DcsParamState, parser.DcsIntermediateState, parser.DcsStringState: + if p.handler.HandleDcs != nil { + p.handler.HandleDcs(Cmd(p.cmd), p.Params(), data) + } + case parser.OscStringState: + if p.handler.HandleOsc != nil { + p.handler.HandleOsc(p.cmd, data) + } + case parser.SosStringState: + if p.handler.HandleSos != nil { + p.handler.HandleSos(data) + } + case parser.PmStringState: + if p.handler.HandlePm != nil { + p.handler.HandlePm(data) + } + case parser.ApcStringState: + if p.handler.HandleApc != nil { + p.handler.HandleApc(data) + } + } + } +} + +func utf8ByteLen(b byte) int { + if b <= 0b0111_1111 { // 0x00-0x7F + return 1 + } else if b >= 0b1100_0000 && b <= 0b1101_1111 { // 0xC0-0xDF + return 2 + } else if b >= 0b1110_0000 && b <= 0b1110_1111 { // 0xE0-0xEF + return 3 + } else if b >= 0b1111_0000 && b <= 0b1111_0111 { // 0xF0-0xF7 + return 4 + } + return -1 +} diff --git a/vendor/github.com/charmbracelet/x/ansi/parser/const.go b/vendor/github.com/charmbracelet/x/ansi/parser/const.go new file mode 100644 index 0000000..d62dbf3 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/parser/const.go @@ -0,0 +1,78 @@ +package parser + +// Action is a DEC ANSI parser action. +type Action = byte + +// These are the actions that the parser can take. +const ( + NoneAction Action = iota + ClearAction + CollectAction + PrefixAction + DispatchAction + ExecuteAction + StartAction // Start of a data string + PutAction // Put into the data string + ParamAction + PrintAction + + IgnoreAction = NoneAction +) + +// nolint: unused +var ActionNames = []string{ + "NoneAction", + "ClearAction", + "CollectAction", + "PrefixAction", + "DispatchAction", + "ExecuteAction", + "StartAction", + "PutAction", + "ParamAction", + "PrintAction", +} + +// State is a DEC ANSI parser state. +type State = byte + +// These are the states that the parser can be in. +const ( + GroundState State = iota + CsiEntryState + CsiIntermediateState + CsiParamState + DcsEntryState + DcsIntermediateState + DcsParamState + DcsStringState + EscapeState + EscapeIntermediateState + OscStringState + SosStringState + PmStringState + ApcStringState + + // Utf8State is not part of the DEC ANSI standard. It is used to handle + // UTF-8 sequences. + Utf8State +) + +// nolint: unused +var StateNames = []string{ + "GroundState", + "CsiEntryState", + "CsiIntermediateState", + "CsiParamState", + "DcsEntryState", + "DcsIntermediateState", + "DcsParamState", + "DcsStringState", + "EscapeState", + "EscapeIntermediateState", + "OscStringState", + "SosStringState", + "PmStringState", + "ApcStringState", + "Utf8State", +} diff --git a/vendor/github.com/charmbracelet/x/ansi/parser/seq.go b/vendor/github.com/charmbracelet/x/ansi/parser/seq.go new file mode 100644 index 0000000..29f491d --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/parser/seq.go @@ -0,0 +1,136 @@ +package parser + +import "math" + +// Shift and masks for sequence parameters and intermediates. +const ( + PrefixShift = 8 + IntermedShift = 16 + FinalMask = 0xff + HasMoreFlag = math.MinInt32 + ParamMask = ^HasMoreFlag + MissingParam = ParamMask + MissingCommand = MissingParam + MaxParam = math.MaxUint16 // the maximum value a parameter can have +) + +const ( + // MaxParamsSize is the maximum number of parameters a sequence can have. + MaxParamsSize = 32 + + // DefaultParamValue is the default value used for missing parameters. + DefaultParamValue = 0 +) + +// 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 prefix. +func Prefix(cmd int) int { + return (cmd >> PrefixShift) & FinalMask +} + +// Intermediate returns the intermediate byte of the 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 Intermediate(cmd int) int { + return (cmd >> IntermedShift) & FinalMask +} + +// Command returns the command byte of the CSI sequence. +func Command(cmd int) int { + return cmd & FinalMask +} + +// Param returns the parameter at the given index. +// It returns -1 if the parameter does not exist. +func Param(params []int, i int) int { + if len(params) == 0 || i < 0 || i >= len(params) { + return -1 + } + + p := params[i] & ParamMask + if p == MissingParam { + return -1 + } + + return p +} + +// HasMore returns true if the parameter has more sub-parameters. +func HasMore(params []int, i int) bool { + if len(params) == 0 || i >= len(params) { + return false + } + + return params[i]&HasMoreFlag != 0 +} + +// Subparams returns the sub-parameters of the given parameter. +// It returns nil if the parameter does not exist. +func Subparams(params []int, i int) []int { + if len(params) == 0 || i < 0 || i >= len(params) { + return nil + } + + // Count the number of parameters before the given parameter index. + var count int + var j int + for j = 0; j < len(params); j++ { + if count == i { + break + } + if !HasMore(params, j) { + count++ + } + } + + if count > i || j >= len(params) { + return nil + } + + var subs []int + for ; j < len(params); j++ { + if !HasMore(params, j) { + break + } + p := Param(params, j) + if p == -1 { + p = DefaultParamValue + } + subs = append(subs, p) + } + + p := Param(params, j) + if p == -1 { + p = DefaultParamValue + } + + return append(subs, p) +} + +// Len returns the number of parameters in the sequence. +// This will return the number of parameters in the sequence, excluding any +// sub-parameters. +func Len(params []int) int { + var n int + for i := 0; i < len(params); i++ { + if !HasMore(params, i) { + n++ + } + } + return n +} + +// Range iterates over the parameters of the sequence and calls the given +// function for each parameter. +// The function should return false to stop the iteration. +func Range(params []int, fn func(i int, param int, hasMore bool) bool) { + for i := 0; i < len(params); i++ { + if !fn(i, Param(params, i), HasMore(params, i)) { + break + } + } +} diff --git a/vendor/github.com/charmbracelet/x/ansi/parser/transition_table.go b/vendor/github.com/charmbracelet/x/ansi/parser/transition_table.go new file mode 100644 index 0000000..558a5ea --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/parser/transition_table.go @@ -0,0 +1,273 @@ +package parser + +// Table values are generated like this: +// +// index: currentState << IndexStateShift | charCode +// value: action << TransitionActionShift | nextState +const ( + TransitionActionShift = 4 + TransitionStateMask = 15 + IndexStateShift = 8 + + // DefaultTableSize is the default size of the transition table. + DefaultTableSize = 4096 +) + +// Table is a DEC ANSI transition table. +var Table = GenerateTransitionTable() + +// TransitionTable is a DEC ANSI transition table. +// https://vt100.net/emu/dec_ansi_parser +type TransitionTable []byte + +// NewTransitionTable returns a new DEC ANSI transition table. +func NewTransitionTable(size int) TransitionTable { + if size <= 0 { + size = DefaultTableSize + } + return TransitionTable(make([]byte, size)) +} + +// SetDefault sets default transition. +func (t TransitionTable) SetDefault(action Action, state State) { + for i := 0; i < len(t); i++ { + t[i] = action<<TransitionActionShift | state + } +} + +// AddOne adds a transition. +func (t TransitionTable) AddOne(code byte, state State, action Action, next State) { + idx := int(state)<<IndexStateShift | int(code) + value := action<<TransitionActionShift | next + t[idx] = value +} + +// AddMany adds many transitions. +func (t TransitionTable) AddMany(codes []byte, state State, action Action, next State) { + for _, code := range codes { + t.AddOne(code, state, action, next) + } +} + +// AddRange adds a range of transitions. +func (t TransitionTable) AddRange(start, end byte, state State, action Action, next State) { + for i := int(start); i <= int(end); i++ { + t.AddOne(byte(i), state, action, next) + } +} + +// Transition returns the next state and action for the given state and byte. +func (t TransitionTable) Transition(state State, code byte) (State, Action) { + index := int(state)<<IndexStateShift | int(code) + value := t[index] + return value & TransitionStateMask, value >> TransitionActionShift +} + +// byte range macro +func r(start, end byte) []byte { + var a []byte + for i := int(start); i <= int(end); i++ { + a = append(a, byte(i)) + } + return a +} + +// GenerateTransitionTable generates a DEC ANSI transition table compatible +// with the VT500-series of terminals. This implementation includes a few +// modifications that include: +// - A new Utf8State is introduced to handle UTF8 sequences. +// - Osc and Dcs data accept UTF8 sequences by extending the printable range +// to 0xFF and 0xFE respectively. +// - We don't ignore 0x3A (':') when building Csi and Dcs parameters and +// instead use it to denote sub-parameters. +// - Support dispatching SosPmApc sequences. +// - The DEL (0x7F) character is executed in the Ground state. +// - The DEL (0x7F) character is collected in the DcsPassthrough string state. +// - The ST C1 control character (0x9C) is executed and not ignored. +func GenerateTransitionTable() TransitionTable { + table := NewTransitionTable(DefaultTableSize) + table.SetDefault(NoneAction, GroundState) + + // Anywhere + for _, state := range r(GroundState, Utf8State) { + // Anywhere -> Ground + table.AddMany([]byte{0x18, 0x1a, 0x99, 0x9a}, state, ExecuteAction, GroundState) + table.AddRange(0x80, 0x8F, state, ExecuteAction, GroundState) + table.AddRange(0x90, 0x97, state, ExecuteAction, GroundState) + table.AddOne(0x9C, state, ExecuteAction, GroundState) + // Anywhere -> Escape + table.AddOne(0x1B, state, ClearAction, EscapeState) + // Anywhere -> SosStringState + table.AddOne(0x98, state, StartAction, SosStringState) + // Anywhere -> PmStringState + table.AddOne(0x9E, state, StartAction, PmStringState) + // Anywhere -> ApcStringState + table.AddOne(0x9F, state, StartAction, ApcStringState) + // Anywhere -> CsiEntry + table.AddOne(0x9B, state, ClearAction, CsiEntryState) + // Anywhere -> DcsEntry + table.AddOne(0x90, state, ClearAction, DcsEntryState) + // Anywhere -> OscString + table.AddOne(0x9D, state, StartAction, OscStringState) + // Anywhere -> Utf8 + table.AddRange(0xC2, 0xDF, state, CollectAction, Utf8State) // UTF8 2 byte sequence + table.AddRange(0xE0, 0xEF, state, CollectAction, Utf8State) // UTF8 3 byte sequence + table.AddRange(0xF0, 0xF4, state, CollectAction, Utf8State) // UTF8 4 byte sequence + } + + // Ground + table.AddRange(0x00, 0x17, GroundState, ExecuteAction, GroundState) + table.AddOne(0x19, GroundState, ExecuteAction, GroundState) + table.AddRange(0x1C, 0x1F, GroundState, ExecuteAction, GroundState) + table.AddRange(0x20, 0x7E, GroundState, PrintAction, GroundState) + table.AddOne(0x7F, GroundState, ExecuteAction, GroundState) + + // EscapeIntermediate + table.AddRange(0x00, 0x17, EscapeIntermediateState, ExecuteAction, EscapeIntermediateState) + table.AddOne(0x19, EscapeIntermediateState, ExecuteAction, EscapeIntermediateState) + table.AddRange(0x1C, 0x1F, EscapeIntermediateState, ExecuteAction, EscapeIntermediateState) + table.AddRange(0x20, 0x2F, EscapeIntermediateState, CollectAction, EscapeIntermediateState) + table.AddOne(0x7F, EscapeIntermediateState, IgnoreAction, EscapeIntermediateState) + // EscapeIntermediate -> Ground + table.AddRange(0x30, 0x7E, EscapeIntermediateState, DispatchAction, GroundState) + + // Escape + table.AddRange(0x00, 0x17, EscapeState, ExecuteAction, EscapeState) + table.AddOne(0x19, EscapeState, ExecuteAction, EscapeState) + table.AddRange(0x1C, 0x1F, EscapeState, ExecuteAction, EscapeState) + table.AddOne(0x7F, EscapeState, IgnoreAction, EscapeState) + // Escape -> Ground + table.AddRange(0x30, 0x4F, EscapeState, DispatchAction, GroundState) + table.AddRange(0x51, 0x57, EscapeState, DispatchAction, GroundState) + table.AddOne(0x59, EscapeState, DispatchAction, GroundState) + table.AddOne(0x5A, EscapeState, DispatchAction, GroundState) + table.AddOne(0x5C, EscapeState, DispatchAction, GroundState) + table.AddRange(0x60, 0x7E, EscapeState, DispatchAction, GroundState) + // Escape -> Escape_intermediate + table.AddRange(0x20, 0x2F, EscapeState, CollectAction, EscapeIntermediateState) + // Escape -> Sos_pm_apc_string + table.AddOne('X', EscapeState, StartAction, SosStringState) // SOS + table.AddOne('^', EscapeState, StartAction, PmStringState) // PM + table.AddOne('_', EscapeState, StartAction, ApcStringState) // APC + // Escape -> Dcs_entry + table.AddOne('P', EscapeState, ClearAction, DcsEntryState) + // Escape -> Csi_entry + table.AddOne('[', EscapeState, ClearAction, CsiEntryState) + // Escape -> Osc_string + table.AddOne(']', EscapeState, StartAction, OscStringState) + + // Sos_pm_apc_string + for _, state := range r(SosStringState, ApcStringState) { + table.AddRange(0x00, 0x17, state, PutAction, state) + table.AddOne(0x19, state, PutAction, state) + table.AddRange(0x1C, 0x1F, state, PutAction, state) + table.AddRange(0x20, 0x7F, state, PutAction, state) + // ESC, ST, CAN, and SUB terminate the sequence + table.AddOne(0x1B, state, DispatchAction, EscapeState) + table.AddOne(0x9C, state, DispatchAction, GroundState) + table.AddMany([]byte{0x18, 0x1A}, state, IgnoreAction, GroundState) + } + + // Dcs_entry + table.AddRange(0x00, 0x07, DcsEntryState, IgnoreAction, DcsEntryState) + table.AddRange(0x0E, 0x17, DcsEntryState, IgnoreAction, DcsEntryState) + table.AddOne(0x19, DcsEntryState, IgnoreAction, DcsEntryState) + table.AddRange(0x1C, 0x1F, DcsEntryState, IgnoreAction, DcsEntryState) + table.AddOne(0x7F, DcsEntryState, IgnoreAction, DcsEntryState) + // Dcs_entry -> Dcs_intermediate + table.AddRange(0x20, 0x2F, DcsEntryState, CollectAction, DcsIntermediateState) + // Dcs_entry -> Dcs_param + table.AddRange(0x30, 0x3B, DcsEntryState, ParamAction, 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 + // passthrough of ANSI sequences like in Screen or Tmux passthrough mode. + table.AddOne(0x1B, DcsEntryState, PutAction, DcsStringState) + table.AddRange(0x40, 0x7E, DcsEntryState, StartAction, DcsStringState) + + // Dcs_intermediate + table.AddRange(0x00, 0x17, DcsIntermediateState, IgnoreAction, DcsIntermediateState) + table.AddOne(0x19, DcsIntermediateState, IgnoreAction, DcsIntermediateState) + table.AddRange(0x1C, 0x1F, DcsIntermediateState, IgnoreAction, DcsIntermediateState) + table.AddRange(0x20, 0x2F, DcsIntermediateState, CollectAction, DcsIntermediateState) + table.AddOne(0x7F, DcsIntermediateState, IgnoreAction, DcsIntermediateState) + // Dcs_intermediate -> Dcs_passthrough + table.AddRange(0x30, 0x3F, DcsIntermediateState, StartAction, DcsStringState) + table.AddRange(0x40, 0x7E, DcsIntermediateState, StartAction, DcsStringState) + + // Dcs_param + table.AddRange(0x00, 0x17, DcsParamState, IgnoreAction, DcsParamState) + table.AddOne(0x19, DcsParamState, IgnoreAction, DcsParamState) + table.AddRange(0x1C, 0x1F, DcsParamState, IgnoreAction, DcsParamState) + table.AddRange(0x30, 0x3B, DcsParamState, ParamAction, DcsParamState) + table.AddOne(0x7F, DcsParamState, IgnoreAction, DcsParamState) + table.AddRange(0x3C, 0x3F, DcsParamState, IgnoreAction, DcsParamState) + // Dcs_param -> Dcs_intermediate + table.AddRange(0x20, 0x2F, DcsParamState, CollectAction, DcsIntermediateState) + // Dcs_param -> Dcs_passthrough + table.AddRange(0x40, 0x7E, DcsParamState, StartAction, DcsStringState) + + // Dcs_passthrough + table.AddRange(0x00, 0x17, DcsStringState, PutAction, DcsStringState) + table.AddOne(0x19, DcsStringState, PutAction, DcsStringState) + table.AddRange(0x1C, 0x1F, DcsStringState, PutAction, DcsStringState) + table.AddRange(0x20, 0x7E, DcsStringState, PutAction, DcsStringState) + table.AddOne(0x7F, DcsStringState, PutAction, DcsStringState) + table.AddRange(0x80, 0xFF, DcsStringState, PutAction, DcsStringState) // Allow Utf8 characters by extending the printable range from 0x7F to 0xFF + // ST, CAN, SUB, and ESC terminate the sequence + table.AddOne(0x1B, DcsStringState, DispatchAction, EscapeState) + table.AddOne(0x9C, DcsStringState, DispatchAction, GroundState) + table.AddMany([]byte{0x18, 0x1A}, DcsStringState, IgnoreAction, GroundState) + + // Csi_param + table.AddRange(0x00, 0x17, CsiParamState, ExecuteAction, CsiParamState) + table.AddOne(0x19, CsiParamState, ExecuteAction, CsiParamState) + table.AddRange(0x1C, 0x1F, CsiParamState, ExecuteAction, CsiParamState) + table.AddRange(0x30, 0x3B, CsiParamState, ParamAction, CsiParamState) + table.AddOne(0x7F, CsiParamState, IgnoreAction, CsiParamState) + table.AddRange(0x3C, 0x3F, CsiParamState, IgnoreAction, CsiParamState) + // Csi_param -> Ground + table.AddRange(0x40, 0x7E, CsiParamState, DispatchAction, GroundState) + // Csi_param -> Csi_intermediate + table.AddRange(0x20, 0x2F, CsiParamState, CollectAction, CsiIntermediateState) + + // Csi_intermediate + table.AddRange(0x00, 0x17, CsiIntermediateState, ExecuteAction, CsiIntermediateState) + table.AddOne(0x19, CsiIntermediateState, ExecuteAction, CsiIntermediateState) + table.AddRange(0x1C, 0x1F, CsiIntermediateState, ExecuteAction, CsiIntermediateState) + table.AddRange(0x20, 0x2F, CsiIntermediateState, CollectAction, CsiIntermediateState) + table.AddOne(0x7F, CsiIntermediateState, IgnoreAction, CsiIntermediateState) + // Csi_intermediate -> Ground + table.AddRange(0x40, 0x7E, CsiIntermediateState, DispatchAction, GroundState) + // Csi_intermediate -> Csi_ignore + table.AddRange(0x30, 0x3F, CsiIntermediateState, IgnoreAction, GroundState) + + // Csi_entry + table.AddRange(0x00, 0x17, CsiEntryState, ExecuteAction, CsiEntryState) + table.AddOne(0x19, CsiEntryState, ExecuteAction, CsiEntryState) + table.AddRange(0x1C, 0x1F, CsiEntryState, ExecuteAction, CsiEntryState) + table.AddOne(0x7F, CsiEntryState, IgnoreAction, CsiEntryState) + // Csi_entry -> Ground + table.AddRange(0x40, 0x7E, CsiEntryState, DispatchAction, GroundState) + // Csi_entry -> Csi_intermediate + table.AddRange(0x20, 0x2F, CsiEntryState, CollectAction, CsiIntermediateState) + // Csi_entry -> Csi_param + table.AddRange(0x30, 0x3B, CsiEntryState, ParamAction, CsiParamState) + table.AddRange(0x3C, 0x3F, CsiEntryState, PrefixAction, CsiParamState) + + // Osc_string + table.AddRange(0x00, 0x06, OscStringState, IgnoreAction, OscStringState) + table.AddRange(0x08, 0x17, OscStringState, IgnoreAction, OscStringState) + table.AddOne(0x19, OscStringState, IgnoreAction, OscStringState) + table.AddRange(0x1C, 0x1F, OscStringState, IgnoreAction, OscStringState) + table.AddRange(0x20, 0xFF, OscStringState, PutAction, OscStringState) // Allow Utf8 characters by extending the printable range from 0x7F to 0xFF + + // ST, CAN, SUB, ESC, and BEL terminate the sequence + table.AddOne(0x1B, OscStringState, DispatchAction, EscapeState) + table.AddOne(0x07, OscStringState, DispatchAction, GroundState) + table.AddOne(0x9C, OscStringState, DispatchAction, GroundState) + table.AddMany([]byte{0x18, 0x1A}, OscStringState, IgnoreAction, GroundState) + + return table +} diff --git a/vendor/github.com/charmbracelet/x/ansi/parser_decode.go b/vendor/github.com/charmbracelet/x/ansi/parser_decode.go new file mode 100644 index 0000000..3e50473 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/parser_decode.go @@ -0,0 +1,524 @@ +package ansi + +import ( + "unicode/utf8" + + "github.com/charmbracelet/x/ansi/parser" + "github.com/mattn/go-runewidth" + "github.com/rivo/uniseg" +) + +// State represents the state of the ANSI escape sequence parser used by +// [DecodeSequence]. +type State = byte + +// ANSI escape sequence states used by [DecodeSequence]. +const ( + NormalState State = iota + PrefixState + ParamsState + IntermedState + EscapeState + StringState +) + +// 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:] +// } +// +// 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] + + switch state { + case NormalState: + switch c { + case ESC: + if p != nil { + if len(p.params) > 0 { + p.params[0] = parser.MissingParam + } + p.cmd = 0 + p.paramsLen = 0 + p.dataLen = 0 + } + state = EscapeState + continue + case CSI, DCS: + if p != nil { + if len(p.params) > 0 { + p.params[0] = parser.MissingParam + } + p.cmd = 0 + p.paramsLen = 0 + p.dataLen = 0 + } + state = PrefixState + continue + case OSC, APC, SOS, PM: + if p != nil { + p.cmd = parser.MissingCommand + p.dataLen = 0 + } + state = StringState + continue + } + + if p != nil { + p.dataLen = 0 + p.paramsLen = 0 + p.cmd = 0 + } + if c > US && c < DEL { + // ASCII printable characters + return b[i : i+1], 1, 1, NormalState + } + + if c <= US || c == DEL || c < 0xC0 { + // C0 & C1 control characters & DEL + return b[i : i+1], 0, 1, NormalState + } + + 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 PrefixState: + if c >= '<' && c <= '?' { + if p != nil { + // We only collect the last prefix character. + p.cmd &^= 0xff << parser.PrefixShift + p.cmd |= int(c) << parser.PrefixShift + } + break + } + + state = ParamsState + fallthrough + case ParamsState: + if c >= '0' && c <= '9' { + if p != nil { + if p.params[p.paramsLen] == parser.MissingParam { + p.params[p.paramsLen] = 0 + } + + p.params[p.paramsLen] *= 10 + p.params[p.paramsLen] += int(c - '0') + } + break + } + + if c == ':' { + if p != nil { + p.params[p.paramsLen] |= parser.HasMoreFlag + } + } + + if c == ';' || c == ':' { + if p != nil { + p.paramsLen++ + if p.paramsLen < len(p.params) { + p.params[p.paramsLen] = parser.MissingParam + } + } + break + } + + state = IntermedState + fallthrough + case IntermedState: + if c >= ' ' && c <= '/' { + if p != nil { + p.cmd &^= 0xff << parser.IntermedShift + p.cmd |= int(c) << parser.IntermedShift + } + break + } + + if p != nil { + // Increment the last parameter + if p.paramsLen > 0 && p.paramsLen < len(p.params)-1 || + p.paramsLen == 0 && len(p.params) > 0 && p.params[0] != parser.MissingParam { + p.paramsLen++ + } + } + + if c >= '@' && c <= '~' { + if p != nil { + p.cmd &^= 0xff + p.cmd |= int(c) + } + + if HasDcsPrefix(b) { + // Continue to collect DCS data + if p != nil { + p.dataLen = 0 + } + state = StringState + continue + } + + return b[:i+1], 0, i + 1, NormalState + } + + // Invalid CSI/DCS sequence + return b[:i], 0, i, NormalState + case EscapeState: + switch c { + case '[', 'P': + if p != nil { + if len(p.params) > 0 { + p.params[0] = parser.MissingParam + } + p.paramsLen = 0 + p.cmd = 0 + } + state = PrefixState + continue + case ']', 'X', '^', '_': + if p != nil { + p.cmd = parser.MissingCommand + p.dataLen = 0 + } + state = StringState + continue + } + + if c >= ' ' && c <= '/' { + if p != nil { + p.cmd &^= 0xff << parser.IntermedShift + p.cmd |= int(c) << parser.IntermedShift + } + continue + } else if c >= '0' && c <= '~' { + if p != nil { + p.cmd &^= 0xff + p.cmd |= int(c) + } + return b[:i+1], 0, i + 1, NormalState + } + + // Invalid escape sequence + return b[:i], 0, i, NormalState + case StringState: + switch c { + case BEL: + if HasOscPrefix(b) { + parseOscCmd(p) + return b[:i+1], 0, i + 1, NormalState + } + case CAN, SUB: + if HasOscPrefix(b) { + // Ensure we parse the OSC command number + parseOscCmd(p) + } + + // Cancel the sequence + return b[:i], 0, i, NormalState + case ST: + if HasOscPrefix(b) { + // Ensure we parse the OSC command number + parseOscCmd(p) + } + + return b[:i+1], 0, i + 1, NormalState + case ESC: + if HasStPrefix(b[i:]) { + if HasOscPrefix(b) { + // Ensure we parse the OSC command number + parseOscCmd(p) + } + + // End of string 7-bit (ST) + return b[:i+2], 0, i + 2, NormalState + } + + // Otherwise, cancel the sequence + return b[:i], 0, i, NormalState + } + + if p != nil && p.dataLen < len(p.data) { + p.data[p.dataLen] = c + p.dataLen++ + + // Parse the OSC command number + if c == ';' && HasOscPrefix(b) { + parseOscCmd(p) + } + } + } + } + + return b, 0, len(b), state +} + +func parseOscCmd(p *Parser) { + if p == nil || p.cmd != parser.MissingCommand { + return + } + for j := 0; j < p.dataLen; j++ { + d := p.data[j] + if d < '0' || d > '9' { + break + } + if p.cmd == parser.MissingCommand { + p.cmd = 0 + } + p.cmd *= 10 + p.cmd += int(d - '0') + } +} + +// Equal returns true if the given byte slices are equal. +func Equal[T string | []byte](a, b T) bool { + return string(a) == string(b) +} + +// HasPrefix returns true if the given byte slice has prefix. +func HasPrefix[T string | []byte](b, prefix T) bool { + return len(b) >= len(prefix) && Equal(b[0:len(prefix)], prefix) +} + +// HasSuffix returns true if the given byte slice has suffix. +func HasSuffix[T string | []byte](b, suffix T) bool { + return len(b) >= len(suffix) && Equal(b[len(b)-len(suffix):], suffix) +} + +// HasCsiPrefix returns true if the given byte slice has a CSI prefix. +func HasCsiPrefix[T string | []byte](b T) bool { + return (len(b) > 0 && b[0] == CSI) || + (len(b) > 1 && b[0] == ESC && b[1] == '[') +} + +// HasOscPrefix returns true if the given byte slice has an OSC prefix. +func HasOscPrefix[T string | []byte](b T) bool { + return (len(b) > 0 && b[0] == OSC) || + (len(b) > 1 && b[0] == ESC && b[1] == ']') +} + +// HasApcPrefix returns true if the given byte slice has an APC prefix. +func HasApcPrefix[T string | []byte](b T) bool { + return (len(b) > 0 && b[0] == APC) || + (len(b) > 1 && b[0] == ESC && b[1] == '_') +} + +// HasDcsPrefix returns true if the given byte slice has a DCS prefix. +func HasDcsPrefix[T string | []byte](b T) bool { + return (len(b) > 0 && b[0] == DCS) || + (len(b) > 1 && b[0] == ESC && b[1] == 'P') +} + +// HasSosPrefix returns true if the given byte slice has a SOS prefix. +func HasSosPrefix[T string | []byte](b T) bool { + return (len(b) > 0 && b[0] == SOS) || + (len(b) > 1 && b[0] == ESC && b[1] == 'X') +} + +// HasPmPrefix returns true if the given byte slice has a PM prefix. +func HasPmPrefix[T string | []byte](b T) bool { + return (len(b) > 0 && b[0] == PM) || + (len(b) > 1 && b[0] == ESC && b[1] == '^') +} + +// HasStPrefix returns true if the given byte slice has a ST prefix. +func HasStPrefix[T string | []byte](b T) bool { + return (len(b) > 0 && b[0] == ST) || + (len(b) > 1 && b[0] == ESC && b[1] == '\\') +} + +// HasEscPrefix returns true if the given byte slice has an ESC prefix. +func HasEscPrefix[T string | []byte](b T) bool { + return len(b) > 0 && b[0] == ESC +} + +// FirstGraphemeCluster returns the first grapheme cluster in the given string or byte slice. +// This is a syntactic sugar function that wraps +// uniseg.FirstGraphemeClusterInString and uniseg.FirstGraphemeCluster. +func FirstGraphemeCluster[T string | []byte](b T, state int) (T, T, int, int) { + switch b := any(b).(type) { + case string: + cluster, rest, width, newState := uniseg.FirstGraphemeClusterInString(b, state) + return T(cluster), T(rest), width, newState + case []byte: + cluster, rest, width, newState := uniseg.FirstGraphemeCluster(b, state) + return T(cluster), T(rest), width, newState + } + panic("unreachable") +} + +// 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 Cmd int + +// 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 prefix. +func (c Cmd) Prefix() byte { + return byte(parser.Prefix(int(c))) +} + +// Intermediate returns the unpacked 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 (c Cmd) Intermediate() byte { + return byte(parser.Intermediate(int(c))) +} + +// Final returns the unpacked command byte of the CSI sequence. +func (c Cmd) Final() byte { + return byte(parser.Command(int(c))) +} + +// Command packs a command with the given prefix, intermediate, and final. A +// zero byte means the sequence does not have a prefix or intermediate. +// +// 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 +// `@A–Z[\]^_`a–z{|}~`. +func Command(prefix, inter, final byte) (c int) { + c = int(final) + c |= int(prefix) << parser.PrefixShift + c |= int(inter) << parser.IntermedShift + return +} + +// 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 Param int + +// Param returns the unpacked parameter at the given index. +// It returns the default value if the parameter is missing. +func (s Param) Param(def int) int { + p := int(s) & parser.ParamMask + if p == parser.MissingParam { + return def + } + return p +} + +// HasMore unpacks the HasMoreFlag from the parameter. +func (s Param) HasMore() bool { + return s&parser.HasMoreFlag != 0 +} + +// 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 |= parser.HasMoreFlag + } + return +} diff --git a/vendor/github.com/charmbracelet/x/ansi/parser_handler.go b/vendor/github.com/charmbracelet/x/ansi/parser_handler.go new file mode 100644 index 0000000..03f9ed4 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/parser_handler.go @@ -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(¶ms[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 +} diff --git a/vendor/github.com/charmbracelet/x/ansi/parser_sync.go b/vendor/github.com/charmbracelet/x/ansi/parser_sync.go new file mode 100644 index 0000000..65d25a9 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/parser_sync.go @@ -0,0 +1,29 @@ +package ansi + +import ( + "sync" + + "github.com/charmbracelet/x/ansi/parser" +) + +var parserPool = sync.Pool{ + New: func() any { + p := NewParser() + p.SetParamsSize(parser.MaxParamsSize) + p.SetDataSize(1024 * 1024 * 4) // 4MB of data buffer + return p + }, +} + +// GetParser returns a parser from a sync pool. +func GetParser() *Parser { + return parserPool.Get().(*Parser) +} + +// PutParser returns a parser to a sync pool. The parser is reset +// automatically. +func PutParser(p *Parser) { + p.Reset() + p.dataLen = 0 + parserPool.Put(p) +} diff --git a/vendor/github.com/charmbracelet/x/ansi/passthrough.go b/vendor/github.com/charmbracelet/x/ansi/passthrough.go new file mode 100644 index 0000000..14a7452 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/passthrough.go @@ -0,0 +1,63 @@ +package ansi + +import ( + "bytes" +) + +// ScreenPassthrough wraps the given ANSI sequence in a DCS passthrough +// sequence to be sent to the outer terminal. This is used to send raw escape +// sequences to the outer terminal when running inside GNU Screen. +// +// DCS <data> ST +// +// Note: Screen limits the length of string sequences to 768 bytes (since 2014). +// Use zero to indicate no limit, otherwise, this will chunk the returned +// string into limit sized chunks. +// +// See: https://www.gnu.org/software/screen/manual/screen.html#String-Escapes +// See: https://git.savannah.gnu.org/cgit/screen.git/tree/src/screen.h?id=c184c6ec27683ff1a860c45be5cf520d896fd2ef#n44 +func ScreenPassthrough(seq string, limit int) string { + var b bytes.Buffer + b.WriteString("\x1bP") + if limit > 0 { + for i := 0; i < len(seq); i += limit { + end := i + limit + if end > len(seq) { + end = len(seq) + } + b.WriteString(seq[i:end]) + if end < len(seq) { + b.WriteString("\x1b\\\x1bP") + } + } + } else { + b.WriteString(seq) + } + b.WriteString("\x1b\\") + return b.String() +} + +// TmuxPassthrough wraps the given ANSI sequence in a special DCS passthrough +// sequence to be sent to the outer terminal. This is used to send raw escape +// sequences to the outer terminal when running inside Tmux. +// +// DCS tmux ; <escaped-data> ST +// +// Where <escaped-data> is the given sequence in which all occurrences of ESC +// (0x1b) are doubled i.e. replaced with ESC ESC (0x1b 0x1b). +// +// Note: this needs the `allow-passthrough` option to be set to `on`. +// +// See: https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it +func TmuxPassthrough(seq string) string { + var b bytes.Buffer + b.WriteString("\x1bPtmux;") + for i := 0; i < len(seq); i++ { + if seq[i] == ESC { + b.WriteByte(ESC) + } + b.WriteByte(seq[i]) + } + b.WriteString("\x1b\\") + return b.String() +} diff --git a/vendor/github.com/charmbracelet/x/ansi/paste.go b/vendor/github.com/charmbracelet/x/ansi/paste.go new file mode 100644 index 0000000..2f9ea6f --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/paste.go @@ -0,0 +1,7 @@ +package ansi + +// BracketedPasteStart is the control sequence to enable bracketed paste mode. +const BracketedPasteStart = "\x1b[200~" + +// BracketedPasteEnd is the control sequence to disable bracketed paste mode. +const BracketedPasteEnd = "\x1b[201~" diff --git a/vendor/github.com/charmbracelet/x/ansi/reset.go b/vendor/github.com/charmbracelet/x/ansi/reset.go new file mode 100644 index 0000000..c1b89ea --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/reset.go @@ -0,0 +1,11 @@ +package ansi + +// ResetInitialState (RIS) resets the terminal to its initial state. +// +// ESC c +// +// See: https://vt100.net/docs/vt510-rm/RIS.html +const ( + ResetInitialState = "\x1bc" + RIS = ResetInitialState +) diff --git a/vendor/github.com/charmbracelet/x/ansi/screen.go b/vendor/github.com/charmbracelet/x/ansi/screen.go new file mode 100644 index 0000000..c76e4f0 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/screen.go @@ -0,0 +1,410 @@ +package ansi + +import ( + "strconv" + "strings" +) + +// EraseDisplay (ED) clears the display or parts of the display. A screen is +// the shown part of the terminal display excluding the scrollback buffer. +// Possible values: +// +// Default is 0. +// +// 0: Clear from cursor to end of screen. +// 1: Clear from cursor to beginning of the screen. +// 2: Clear entire screen (and moves cursor to upper left on DOS). +// 3: Clear entire display which delete all lines saved in the scrollback buffer (xterm). +// +// CSI <n> J +// +// See: https://vt100.net/docs/vt510-rm/ED.html +func EraseDisplay(n int) string { + var s string + if n > 0 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "J" +} + +// ED is an alias for [EraseDisplay]. +func ED(n int) string { + return EraseDisplay(n) +} + +// EraseDisplay constants. +// These are the possible values for the EraseDisplay function. +const ( + EraseScreenBelow = "\x1b[J" + EraseScreenAbove = "\x1b[1J" + EraseEntireScreen = "\x1b[2J" + EraseEntireDisplay = "\x1b[3J" +) + +// EraseLine (EL) clears the current line or parts of the line. Possible values: +// +// 0: Clear from cursor to end of line. +// 1: Clear from cursor to beginning of the line. +// 2: Clear entire line. +// +// The cursor position is not affected. +// +// CSI <n> K +// +// See: https://vt100.net/docs/vt510-rm/EL.html +func EraseLine(n int) string { + var s string + if n > 0 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "K" +} + +// EL is an alias for [EraseLine]. +func EL(n int) string { + return EraseLine(n) +} + +// EraseLine constants. +// These are the possible values for the EraseLine function. +const ( + EraseLineRight = "\x1b[K" + EraseLineLeft = "\x1b[1K" + EraseEntireLine = "\x1b[2K" +) + +// ScrollUp (SU) scrolls the screen up n lines. New lines are added at the +// bottom of the screen. +// +// CSI Pn S +// +// See: https://vt100.net/docs/vt510-rm/SU.html +func ScrollUp(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "S" +} + +// PanDown is an alias for [ScrollUp]. +func PanDown(n int) string { + return ScrollUp(n) +} + +// SU is an alias for [ScrollUp]. +func SU(n int) string { + return ScrollUp(n) +} + +// ScrollDown (SD) scrolls the screen down n lines. New lines are added at the +// top of the screen. +// +// CSI Pn T +// +// See: https://vt100.net/docs/vt510-rm/SD.html +func ScrollDown(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "T" +} + +// PanUp is an alias for [ScrollDown]. +func PanUp(n int) string { + return ScrollDown(n) +} + +// SD is an alias for [ScrollDown]. +func SD(n int) string { + return ScrollDown(n) +} + +// InsertLine (IL) inserts n blank lines at the current cursor position. +// Existing lines are moved down. +// +// CSI Pn L +// +// See: https://vt100.net/docs/vt510-rm/IL.html +func InsertLine(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "L" +} + +// IL is an alias for [InsertLine]. +func IL(n int) string { + return InsertLine(n) +} + +// DeleteLine (DL) deletes n lines at the current cursor position. Existing +// lines are moved up. +// +// CSI Pn M +// +// See: https://vt100.net/docs/vt510-rm/DL.html +func DeleteLine(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "M" +} + +// DL is an alias for [DeleteLine]. +func DL(n int) string { + return DeleteLine(n) +} + +// SetTopBottomMargins (DECSTBM) sets the top and bottom margins for the scrolling +// region. The default is the entire screen. +// +// Default is 1 and the bottom of the screen. +// +// CSI Pt ; Pb r +// +// See: https://vt100.net/docs/vt510-rm/DECSTBM.html +func SetTopBottomMargins(top, bot int) string { + var t, b string + if top > 0 { + t = strconv.Itoa(top) + } + if bot > 0 { + b = strconv.Itoa(bot) + } + return "\x1b[" + t + ";" + b + "r" +} + +// DECSTBM is an alias for [SetTopBottomMargins]. +func DECSTBM(top, bot int) string { + return SetTopBottomMargins(top, bot) +} + +// SetLeftRightMargins (DECSLRM) sets the left and right margins for the scrolling +// region. +// +// Default is 1 and the right of the screen. +// +// CSI Pl ; Pr s +// +// See: https://vt100.net/docs/vt510-rm/DECSLRM.html +func SetLeftRightMargins(left, right int) string { + var l, r string + if left > 0 { + l = strconv.Itoa(left) + } + if right > 0 { + r = strconv.Itoa(right) + } + return "\x1b[" + l + ";" + r + "s" +} + +// DECSLRM is an alias for [SetLeftRightMargins]. +func DECSLRM(left, right int) string { + return SetLeftRightMargins(left, right) +} + +// SetScrollingRegion (DECSTBM) sets the top and bottom margins for the scrolling +// region. The default is the entire screen. +// +// 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 { + t = 0 + } + if b < 0 { + b = 0 + } + return "\x1b[" + strconv.Itoa(t) + ";" + strconv.Itoa(b) + "r" +} + +// InsertCharacter (ICH) inserts n blank characters at the current cursor +// position. Existing characters move to the right. Characters moved past the +// right margin are lost. ICH has no effect outside the scrolling margins. +// +// Default is 1. +// +// CSI Pn @ +// +// See: https://vt100.net/docs/vt510-rm/ICH.html +func InsertCharacter(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "@" +} + +// ICH is an alias for [InsertCharacter]. +func ICH(n int) string { + return InsertCharacter(n) +} + +// DeleteCharacter (DCH) deletes n characters at the current cursor position. +// As the characters are deleted, the remaining characters move to the left and +// the cursor remains at the same position. +// +// Default is 1. +// +// CSI Pn P +// +// See: https://vt100.net/docs/vt510-rm/DCH.html +func DeleteCharacter(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "P" +} + +// DCH is an alias for [DeleteCharacter]. +func DCH(n int) string { + return DeleteCharacter(n) +} + +// SetTabEvery8Columns (DECST8C) sets the tab stops at every 8 columns. +// +// CSI ? 5 W +// +// See: https://vt100.net/docs/vt510-rm/DECST8C.html +const ( + SetTabEvery8Columns = "\x1b[?5W" + DECST8C = SetTabEvery8Columns +) + +// HorizontalTabSet (HTS) sets a horizontal tab stop at the current cursor +// column. +// +// This is equivalent to [HTS]. +// +// ESC H +// +// See: https://vt100.net/docs/vt510-rm/HTS.html +const HorizontalTabSet = "\x1bH" + +// TabClear (TBC) clears tab stops. +// +// Default is 0. +// +// Possible values: +// 0: Clear tab stop at the current column. (default) +// 3: Clear all tab stops. +// +// CSI Pn g +// +// See: https://vt100.net/docs/vt510-rm/TBC.html +func TabClear(n int) string { + var s string + if n > 0 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "g" +} + +// TBC is an alias for [TabClear]. +func TBC(n int) string { + return TabClear(n) +} + +// RequestPresentationStateReport (DECRQPSR) requests the terminal to send a +// report of the presentation state. This includes the cursor information [DECCIR], +// and tab stop [DECTABSR] reports. +// +// Default is 0. +// +// Possible values: +// 0: Error, request ignored. +// 1: Cursor information report [DECCIR]. +// 2: Tab stop report [DECTABSR]. +// +// CSI Ps $ w +// +// See: https://vt100.net/docs/vt510-rm/DECRQPSR.html +func RequestPresentationStateReport(n int) string { + var s string + if n > 0 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "$w" +} + +// DECRQPSR is an alias for [RequestPresentationStateReport]. +func DECRQPSR(n int) string { + return RequestPresentationStateReport(n) +} + +// TabStopReport (DECTABSR) is the response to a tab stop report request. +// It reports the tab stops set in the terminal. +// +// The response is a list of tab stops separated by a slash (/) character. +// +// DCS 2 $ u D ... D ST +// +// Where D is a decimal number representing a tab stop. +// +// See: https://vt100.net/docs/vt510-rm/DECTABSR.html +func TabStopReport(stops ...int) string { + var s []string + for _, v := range stops { + s = append(s, strconv.Itoa(v)) + } + return "\x1bP2$u" + strings.Join(s, "/") + "\x1b\\" +} + +// DECTABSR is an alias for [TabStopReport]. +func DECTABSR(stops ...int) string { + return TabStopReport(stops...) +} + +// CursorInformationReport (DECCIR) is the response to a cursor information +// report request. It reports the cursor position, visual attributes, and +// character protection attributes. It also reports the status of origin mode +// [DECOM] and the current active character set. +// +// The response is a list of values separated by a semicolon (;) character. +// +// DCS 1 $ u D ... D ST +// +// Where D is a decimal number representing a value. +// +// See: https://vt100.net/docs/vt510-rm/DECCIR.html +func CursorInformationReport(values ...int) string { + var s []string + for _, v := range values { + s = append(s, strconv.Itoa(v)) + } + return "\x1bP1$u" + strings.Join(s, ";") + "\x1b\\" +} + +// DECCIR is an alias for [CursorInformationReport]. +func DECCIR(values ...int) string { + return CursorInformationReport(values...) +} + +// RepeatPreviousCharacter (REP) repeats the previous character n times. +// This is identical to typing the same character n times. +// +// Default is 1. +// +// CSI Pn b +// +// See: ECMA-48 § 8.3.103 +func RepeatPreviousCharacter(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "b" +} + +// REP is an alias for [RepeatPreviousCharacter]. +func REP(n int) string { + return RepeatPreviousCharacter(n) +} diff --git a/vendor/github.com/charmbracelet/x/ansi/sgr.go b/vendor/github.com/charmbracelet/x/ansi/sgr.go new file mode 100644 index 0000000..1a18c98 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/sgr.go @@ -0,0 +1,95 @@ +package ansi + +import "strconv" + +// Select Graphic Rendition (SGR) is a command that sets display attributes. +// +// Default is 0. +// +// CSI Ps ; Ps ... m +// +// See: https://vt100.net/docs/vt510-rm/SGR.html +func SelectGraphicRendition(ps ...Attr) string { + if len(ps) == 0 { + return ResetStyle + } + + var s Style + for _, p := range ps { + attr, ok := attrStrings[p] + if ok { + s = append(s, attr) + } else { + if p < 0 { + p = 0 + } + s = append(s, strconv.Itoa(p)) + } + } + + return s.String() +} + +// SGR is an alias for [SelectGraphicRendition]. +func SGR(ps ...Attr) string { + return SelectGraphicRendition(ps...) +} + +var attrStrings = map[int]string{ + ResetAttr: "0", + BoldAttr: "1", + FaintAttr: "2", + ItalicAttr: "3", + UnderlineAttr: "4", + SlowBlinkAttr: "5", + RapidBlinkAttr: "6", + ReverseAttr: "7", + ConcealAttr: "8", + StrikethroughAttr: "9", + NoBoldAttr: "21", + NormalIntensityAttr: "22", + NoItalicAttr: "23", + NoUnderlineAttr: "24", + NoBlinkAttr: "25", + NoReverseAttr: "27", + NoConcealAttr: "28", + NoStrikethroughAttr: "29", + BlackForegroundColorAttr: "30", + RedForegroundColorAttr: "31", + GreenForegroundColorAttr: "32", + YellowForegroundColorAttr: "33", + BlueForegroundColorAttr: "34", + MagentaForegroundColorAttr: "35", + CyanForegroundColorAttr: "36", + WhiteForegroundColorAttr: "37", + ExtendedForegroundColorAttr: "38", + DefaultForegroundColorAttr: "39", + BlackBackgroundColorAttr: "40", + RedBackgroundColorAttr: "41", + GreenBackgroundColorAttr: "42", + YellowBackgroundColorAttr: "43", + BlueBackgroundColorAttr: "44", + MagentaBackgroundColorAttr: "45", + CyanBackgroundColorAttr: "46", + WhiteBackgroundColorAttr: "47", + ExtendedBackgroundColorAttr: "48", + DefaultBackgroundColorAttr: "49", + ExtendedUnderlineColorAttr: "58", + DefaultUnderlineColorAttr: "59", + BrightBlackForegroundColorAttr: "90", + BrightRedForegroundColorAttr: "91", + BrightGreenForegroundColorAttr: "92", + BrightYellowForegroundColorAttr: "93", + BrightBlueForegroundColorAttr: "94", + BrightMagentaForegroundColorAttr: "95", + BrightCyanForegroundColorAttr: "96", + BrightWhiteForegroundColorAttr: "97", + BrightBlackBackgroundColorAttr: "100", + BrightRedBackgroundColorAttr: "101", + BrightGreenBackgroundColorAttr: "102", + BrightYellowBackgroundColorAttr: "103", + BrightBlueBackgroundColorAttr: "104", + BrightMagentaBackgroundColorAttr: "105", + BrightCyanBackgroundColorAttr: "106", + BrightWhiteBackgroundColorAttr: "107", +} diff --git a/vendor/github.com/charmbracelet/x/ansi/status.go b/vendor/github.com/charmbracelet/x/ansi/status.go new file mode 100644 index 0000000..4337e18 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/status.go @@ -0,0 +1,144 @@ +package ansi + +import ( + "strconv" + "strings" +) + +// StatusReport represents a terminal status report. +type StatusReport interface { + // StatusReport returns the status report identifier. + StatusReport() int +} + +// ANSIReport represents an ANSI terminal status report. +type ANSIStatusReport int //nolint:revive + +// Report returns the status report identifier. +func (s ANSIStatusReport) StatusReport() int { + return int(s) +} + +// DECStatusReport represents a DEC terminal status report. +type DECStatusReport int + +// Status returns the status report identifier. +func (s DECStatusReport) StatusReport() int { + return int(s) +} + +// DeviceStatusReport (DSR) is a control sequence that reports the terminal's +// status. +// The terminal responds with a DSR sequence. +// +// CSI Ps n +// CSI ? Ps n +// +// If one of the statuses is a [DECStatus], the sequence will use the DEC +// format. +// +// See also https://vt100.net/docs/vt510-rm/DSR.html +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.StatusReport()) + switch status.(type) { + case DECStatusReport: + dec = true + } + } + if dec { + seq += "?" + } + return seq + strings.Join(list, ";") + "n" +} + +// DSR is an alias for [DeviceStatusReport]. +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. +// +// CSI Pl ; Pc R +// +// Where Pl is the line number and Pc is the column number. +// +// See also https://vt100.net/docs/vt510-rm/CPR.html +func CursorPositionReport(line, column int) string { + if line < 1 { + line = 1 + } + if column < 1 { + column = 1 + } + return "\x1b[" + strconv.Itoa(line) + ";" + strconv.Itoa(column) + "R" +} + +// CPR is an alias for [CursorPositionReport]. +func CPR(line, column int) string { + return CursorPositionReport(line, column) +} + +// ExtendedCursorPositionReport (DECXCPR) is a control sequence that reports the +// cursor's position along with the page number (optional). +// +// CSI ? Pl ; Pc R +// CSI ? Pl ; Pc ; Pv R +// +// Where Pl is the line number, Pc is the column number, and Pv is the page +// number. +// +// If the page number is zero or negative, the returned sequence won't include +// the page number. +// +// See also https://vt100.net/docs/vt510-rm/DECXCPR.html +func ExtendedCursorPositionReport(line, column, page int) string { + if line < 1 { + line = 1 + } + if column < 1 { + column = 1 + } + if page < 1 { + return "\x1b[?" + strconv.Itoa(line) + ";" + strconv.Itoa(column) + "R" + } + return "\x1b[?" + strconv.Itoa(line) + ";" + strconv.Itoa(column) + ";" + strconv.Itoa(page) + "R" +} + +// DECXCPR is an alias for [ExtendedCursorPositionReport]. +func DECXCPR(line, column, page int) string { + return ExtendedCursorPositionReport(line, column, page) +} diff --git a/vendor/github.com/charmbracelet/x/ansi/style.go b/vendor/github.com/charmbracelet/x/ansi/style.go new file mode 100644 index 0000000..46ddcaa --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/style.go @@ -0,0 +1,660 @@ +package ansi + +import ( + "image/color" + "strconv" + "strings" +) + +// ResetStyle is a SGR (Select Graphic Rendition) style sequence that resets +// all attributes. +// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters +const ResetStyle = "\x1b[m" + +// Attr is a SGR (Select Graphic Rendition) style attribute. +type Attr = int + +// Style represents an ANSI SGR (Select Graphic Rendition) style. +type Style []string + +// String returns the ANSI SGR (Select Graphic Rendition) style sequence for +// the given style. +func (s Style) String() string { + if len(s) == 0 { + return ResetStyle + } + return "\x1b[" + strings.Join(s, ";") + "m" +} + +// Styled returns a styled string with the given style applied. +func (s Style) Styled(str string) string { + if len(s) == 0 { + return str + } + return s.String() + str + ResetStyle +} + +// Reset appends the reset style attribute to the style. +func (s Style) Reset() Style { + return append(s, resetAttr) +} + +// Bold appends the bold style attribute to the style. +func (s Style) Bold() Style { + return append(s, boldAttr) +} + +// Faint appends the faint style attribute to the style. +func (s Style) Faint() Style { + return append(s, faintAttr) +} + +// Italic appends the italic style attribute to the style. +func (s Style) Italic() Style { + return append(s, italicAttr) +} + +// Underline appends the underline style attribute to the style. +func (s Style) Underline() Style { + return append(s, underlineAttr) +} + +// UnderlineStyle appends the underline style attribute to the style. +func (s Style) UnderlineStyle(u UnderlineStyle) Style { + switch u { + case NoUnderlineStyle: + return s.NoUnderline() + case SingleUnderlineStyle: + return s.Underline() + case DoubleUnderlineStyle: + return append(s, doubleUnderlineStyle) + case CurlyUnderlineStyle: + return append(s, curlyUnderlineStyle) + case DottedUnderlineStyle: + return append(s, dottedUnderlineStyle) + case DashedUnderlineStyle: + return append(s, dashedUnderlineStyle) + } + return s +} + +// DoubleUnderline appends the double underline style attribute to the style. +// This is a convenience method for UnderlineStyle(DoubleUnderlineStyle). +func (s Style) DoubleUnderline() Style { + return s.UnderlineStyle(DoubleUnderlineStyle) +} + +// CurlyUnderline appends the curly underline style attribute to the style. +// This is a convenience method for UnderlineStyle(CurlyUnderlineStyle). +func (s Style) CurlyUnderline() Style { + return s.UnderlineStyle(CurlyUnderlineStyle) +} + +// DottedUnderline appends the dotted underline style attribute to the style. +// This is a convenience method for UnderlineStyle(DottedUnderlineStyle). +func (s Style) DottedUnderline() Style { + return s.UnderlineStyle(DottedUnderlineStyle) +} + +// DashedUnderline appends the dashed underline style attribute to the style. +// This is a convenience method for UnderlineStyle(DashedUnderlineStyle). +func (s Style) DashedUnderline() Style { + return s.UnderlineStyle(DashedUnderlineStyle) +} + +// SlowBlink appends the slow blink style attribute to the style. +func (s Style) SlowBlink() Style { + return append(s, slowBlinkAttr) +} + +// RapidBlink appends the rapid blink style attribute to the style. +func (s Style) RapidBlink() Style { + return append(s, rapidBlinkAttr) +} + +// Reverse appends the reverse style attribute to the style. +func (s Style) Reverse() Style { + return append(s, reverseAttr) +} + +// Conceal appends the conceal style attribute to the style. +func (s Style) Conceal() Style { + return append(s, concealAttr) +} + +// Strikethrough appends the strikethrough style attribute to the style. +func (s Style) Strikethrough() Style { + return append(s, strikethroughAttr) +} + +// NoBold appends the no bold style attribute to the style. +func (s Style) NoBold() Style { + return append(s, noBoldAttr) +} + +// NormalIntensity appends the normal intensity style attribute to the style. +func (s Style) NormalIntensity() Style { + return append(s, normalIntensityAttr) +} + +// NoItalic appends the no italic style attribute to the style. +func (s Style) NoItalic() Style { + return append(s, noItalicAttr) +} + +// NoUnderline appends the no underline style attribute to the style. +func (s Style) NoUnderline() Style { + return append(s, noUnderlineAttr) +} + +// NoBlink appends the no blink style attribute to the style. +func (s Style) NoBlink() Style { + return append(s, noBlinkAttr) +} + +// NoReverse appends the no reverse style attribute to the style. +func (s Style) NoReverse() Style { + return append(s, noReverseAttr) +} + +// NoConceal appends the no conceal style attribute to the style. +func (s Style) NoConceal() Style { + return append(s, noConcealAttr) +} + +// NoStrikethrough appends the no strikethrough style attribute to the style. +func (s Style) NoStrikethrough() Style { + return append(s, noStrikethroughAttr) +} + +// DefaultForegroundColor appends the default foreground color style attribute to the style. +func (s Style) DefaultForegroundColor() Style { + return append(s, defaultForegroundColorAttr) +} + +// DefaultBackgroundColor appends the default background color style attribute to the style. +func (s Style) DefaultBackgroundColor() Style { + return append(s, defaultBackgroundColorAttr) +} + +// DefaultUnderlineColor appends the default underline color style attribute to the style. +func (s Style) DefaultUnderlineColor() Style { + return append(s, defaultUnderlineColorAttr) +} + +// ForegroundColor appends the foreground color style attribute to the style. +func (s Style) ForegroundColor(c Color) Style { + return append(s, foregroundColorString(c)) +} + +// BackgroundColor appends the background color style attribute to the style. +func (s Style) BackgroundColor(c Color) Style { + return append(s, backgroundColorString(c)) +} + +// UnderlineColor appends the underline color style attribute to the style. +func (s Style) UnderlineColor(c Color) Style { + return append(s, underlineColorString(c)) +} + +// UnderlineStyle represents an ANSI SGR (Select Graphic Rendition) underline +// style. +type UnderlineStyle = byte + +const ( + doubleUnderlineStyle = "4:2" + curlyUnderlineStyle = "4:3" + dottedUnderlineStyle = "4:4" + dashedUnderlineStyle = "4:5" +) + +const ( + // NoUnderlineStyle is the default underline style. + NoUnderlineStyle UnderlineStyle = iota + // SingleUnderlineStyle is a single underline style. + SingleUnderlineStyle + // DoubleUnderlineStyle is a double underline style. + DoubleUnderlineStyle + // CurlyUnderlineStyle is a curly underline style. + CurlyUnderlineStyle + // DottedUnderlineStyle is a dotted underline style. + DottedUnderlineStyle + // DashedUnderlineStyle is a dashed underline style. + DashedUnderlineStyle +) + +// SGR (Select Graphic Rendition) style attributes. +// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters +const ( + ResetAttr Attr = 0 + BoldAttr Attr = 1 + FaintAttr Attr = 2 + ItalicAttr Attr = 3 + UnderlineAttr Attr = 4 + SlowBlinkAttr Attr = 5 + RapidBlinkAttr Attr = 6 + ReverseAttr Attr = 7 + ConcealAttr Attr = 8 + StrikethroughAttr Attr = 9 + NoBoldAttr Attr = 21 // Some terminals treat this as double underline. + NormalIntensityAttr Attr = 22 + NoItalicAttr Attr = 23 + NoUnderlineAttr Attr = 24 + NoBlinkAttr Attr = 25 + NoReverseAttr Attr = 27 + NoConcealAttr Attr = 28 + NoStrikethroughAttr Attr = 29 + BlackForegroundColorAttr Attr = 30 + RedForegroundColorAttr Attr = 31 + GreenForegroundColorAttr Attr = 32 + YellowForegroundColorAttr Attr = 33 + BlueForegroundColorAttr Attr = 34 + MagentaForegroundColorAttr Attr = 35 + CyanForegroundColorAttr Attr = 36 + WhiteForegroundColorAttr Attr = 37 + ExtendedForegroundColorAttr Attr = 38 + DefaultForegroundColorAttr Attr = 39 + BlackBackgroundColorAttr Attr = 40 + RedBackgroundColorAttr Attr = 41 + GreenBackgroundColorAttr Attr = 42 + YellowBackgroundColorAttr Attr = 43 + BlueBackgroundColorAttr Attr = 44 + MagentaBackgroundColorAttr Attr = 45 + CyanBackgroundColorAttr Attr = 46 + WhiteBackgroundColorAttr Attr = 47 + ExtendedBackgroundColorAttr Attr = 48 + DefaultBackgroundColorAttr Attr = 49 + ExtendedUnderlineColorAttr Attr = 58 + DefaultUnderlineColorAttr Attr = 59 + BrightBlackForegroundColorAttr Attr = 90 + BrightRedForegroundColorAttr Attr = 91 + BrightGreenForegroundColorAttr Attr = 92 + BrightYellowForegroundColorAttr Attr = 93 + BrightBlueForegroundColorAttr Attr = 94 + BrightMagentaForegroundColorAttr Attr = 95 + BrightCyanForegroundColorAttr Attr = 96 + BrightWhiteForegroundColorAttr Attr = 97 + BrightBlackBackgroundColorAttr Attr = 100 + BrightRedBackgroundColorAttr Attr = 101 + BrightGreenBackgroundColorAttr Attr = 102 + BrightYellowBackgroundColorAttr Attr = 103 + BrightBlueBackgroundColorAttr Attr = 104 + BrightMagentaBackgroundColorAttr Attr = 105 + BrightCyanBackgroundColorAttr Attr = 106 + BrightWhiteBackgroundColorAttr Attr = 107 + + RGBColorIntroducerAttr Attr = 2 + ExtendedColorIntroducerAttr Attr = 5 +) + +const ( + resetAttr = "0" + boldAttr = "1" + faintAttr = "2" + italicAttr = "3" + underlineAttr = "4" + slowBlinkAttr = "5" + rapidBlinkAttr = "6" + reverseAttr = "7" + concealAttr = "8" + strikethroughAttr = "9" + noBoldAttr = "21" + normalIntensityAttr = "22" + noItalicAttr = "23" + noUnderlineAttr = "24" + noBlinkAttr = "25" + noReverseAttr = "27" + noConcealAttr = "28" + noStrikethroughAttr = "29" + blackForegroundColorAttr = "30" + redForegroundColorAttr = "31" + greenForegroundColorAttr = "32" + yellowForegroundColorAttr = "33" + blueForegroundColorAttr = "34" + magentaForegroundColorAttr = "35" + cyanForegroundColorAttr = "36" + whiteForegroundColorAttr = "37" + extendedForegroundColorAttr = "38" + defaultForegroundColorAttr = "39" + blackBackgroundColorAttr = "40" + redBackgroundColorAttr = "41" + greenBackgroundColorAttr = "42" + yellowBackgroundColorAttr = "43" + blueBackgroundColorAttr = "44" + magentaBackgroundColorAttr = "45" + cyanBackgroundColorAttr = "46" + whiteBackgroundColorAttr = "47" + extendedBackgroundColorAttr = "48" + defaultBackgroundColorAttr = "49" + extendedUnderlineColorAttr = "58" + defaultUnderlineColorAttr = "59" + brightBlackForegroundColorAttr = "90" + brightRedForegroundColorAttr = "91" + brightGreenForegroundColorAttr = "92" + brightYellowForegroundColorAttr = "93" + brightBlueForegroundColorAttr = "94" + brightMagentaForegroundColorAttr = "95" + brightCyanForegroundColorAttr = "96" + brightWhiteForegroundColorAttr = "97" + brightBlackBackgroundColorAttr = "100" + brightRedBackgroundColorAttr = "101" + brightGreenBackgroundColorAttr = "102" + brightYellowBackgroundColorAttr = "103" + brightBlueBackgroundColorAttr = "104" + brightMagentaBackgroundColorAttr = "105" + brightCyanBackgroundColorAttr = "106" + brightWhiteBackgroundColorAttr = "107" +) + +// foregroundColorString returns the style SGR attribute for the given +// foreground color. +// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters +func foregroundColorString(c Color) string { + switch c := c.(type) { + case BasicColor: + // 3-bit or 4-bit ANSI foreground + // "3<n>" or "9<n>" where n is the color number from 0 to 7 + switch c { + case Black: + return blackForegroundColorAttr + case Red: + return redForegroundColorAttr + case Green: + return greenForegroundColorAttr + case Yellow: + return yellowForegroundColorAttr + case Blue: + return blueForegroundColorAttr + case Magenta: + return magentaForegroundColorAttr + case Cyan: + return cyanForegroundColorAttr + case White: + return whiteForegroundColorAttr + case BrightBlack: + return brightBlackForegroundColorAttr + case BrightRed: + return brightRedForegroundColorAttr + case BrightGreen: + return brightGreenForegroundColorAttr + case BrightYellow: + return brightYellowForegroundColorAttr + case BrightBlue: + return brightBlueForegroundColorAttr + case BrightMagenta: + return brightMagentaForegroundColorAttr + case BrightCyan: + return brightCyanForegroundColorAttr + case BrightWhite: + return brightWhiteForegroundColorAttr + } + case ExtendedColor: + // 256-color ANSI foreground + // "38;5;<n>" + return "38;5;" + strconv.FormatUint(uint64(c), 10) + case TrueColor, color.Color: + // 24-bit "true color" foreground + // "38;2;<r>;<g>;<b>" + r, g, b, _ := c.RGBA() + return "38;2;" + + strconv.FormatUint(uint64(shift(r)), 10) + ";" + + strconv.FormatUint(uint64(shift(g)), 10) + ";" + + strconv.FormatUint(uint64(shift(b)), 10) + } + return defaultForegroundColorAttr +} + +// backgroundColorString returns the style SGR attribute for the given +// background color. +// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters +func backgroundColorString(c Color) string { + switch c := c.(type) { + case BasicColor: + // 3-bit or 4-bit ANSI foreground + // "4<n>" or "10<n>" where n is the color number from 0 to 7 + switch c { + case Black: + return blackBackgroundColorAttr + case Red: + return redBackgroundColorAttr + case Green: + return greenBackgroundColorAttr + case Yellow: + return yellowBackgroundColorAttr + case Blue: + return blueBackgroundColorAttr + case Magenta: + return magentaBackgroundColorAttr + case Cyan: + return cyanBackgroundColorAttr + case White: + return whiteBackgroundColorAttr + case BrightBlack: + return brightBlackBackgroundColorAttr + case BrightRed: + return brightRedBackgroundColorAttr + case BrightGreen: + return brightGreenBackgroundColorAttr + case BrightYellow: + return brightYellowBackgroundColorAttr + case BrightBlue: + return brightBlueBackgroundColorAttr + case BrightMagenta: + return brightMagentaBackgroundColorAttr + case BrightCyan: + return brightCyanBackgroundColorAttr + case BrightWhite: + return brightWhiteBackgroundColorAttr + } + case ExtendedColor: + // 256-color ANSI foreground + // "48;5;<n>" + return "48;5;" + strconv.FormatUint(uint64(c), 10) + case TrueColor, color.Color: + // 24-bit "true color" foreground + // "38;2;<r>;<g>;<b>" + r, g, b, _ := c.RGBA() + return "48;2;" + + strconv.FormatUint(uint64(shift(r)), 10) + ";" + + strconv.FormatUint(uint64(shift(g)), 10) + ";" + + strconv.FormatUint(uint64(shift(b)), 10) + } + return defaultBackgroundColorAttr +} + +// underlineColorString returns the style SGR attribute for the given underline +// color. +// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters +func underlineColorString(c Color) string { + switch c := c.(type) { + // NOTE: we can't use 3-bit and 4-bit ANSI color codes with underline + // color, use 256-color instead. + // + // 256-color ANSI underline color + // "58;5;<n>" + case BasicColor: + return "58;5;" + strconv.FormatUint(uint64(c), 10) + case ExtendedColor: + return "58;5;" + strconv.FormatUint(uint64(c), 10) + case TrueColor, color.Color: + // 24-bit "true color" foreground + // "38;2;<r>;<g>;<b>" + r, g, b, _ := c.RGBA() + return "58;2;" + + strconv.FormatUint(uint64(shift(r)), 10) + ";" + + strconv.FormatUint(uint64(shift(g)), 10) + ";" + + strconv.FormatUint(uint64(shift(b)), 10) + } + 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 + } +} diff --git a/vendor/github.com/charmbracelet/x/ansi/termcap.go b/vendor/github.com/charmbracelet/x/ansi/termcap.go new file mode 100644 index 0000000..3c5c7da --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/termcap.go @@ -0,0 +1,41 @@ +package ansi + +import ( + "encoding/hex" + "strings" +) + +// RequestTermcap (XTGETTCAP) requests Termcap/Terminfo strings. +// +// DCS + q <Pt> ST +// +// Where <Pt> is a list of Termcap/Terminfo capabilities, encoded in 2-digit +// hexadecimals, separated by semicolons. +// +// See: https://man7.org/linux/man-pages/man5/terminfo.5.html +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func XTGETTCAP(caps ...string) string { + if len(caps) == 0 { + return "" + } + + s := "\x1bP+q" + for i, c := range caps { + if i > 0 { + s += ";" + } + s += strings.ToUpper(hex.EncodeToString([]byte(c))) + } + + return s + "\x1b\\" +} + +// RequestTermcap is an alias for [XTGETTCAP]. +func RequestTermcap(caps ...string) string { + return XTGETTCAP(caps...) +} + +// RequestTerminfo is an alias for [XTGETTCAP]. +func RequestTerminfo(caps ...string) string { + return XTGETTCAP(caps...) +} diff --git a/vendor/github.com/charmbracelet/x/ansi/title.go b/vendor/github.com/charmbracelet/x/ansi/title.go new file mode 100644 index 0000000..8fd8bf9 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/title.go @@ -0,0 +1,32 @@ +package ansi + +// SetIconNameWindowTitle returns a sequence for setting the icon name and +// window title. +// +// OSC 0 ; title ST +// OSC 0 ; title BEL +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands +func SetIconNameWindowTitle(s string) string { + return "\x1b]0;" + s + "\x07" +} + +// SetIconName returns a sequence for setting the icon name. +// +// OSC 1 ; title ST +// OSC 1 ; title BEL +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands +func SetIconName(s string) string { + return "\x1b]1;" + s + "\x07" +} + +// SetWindowTitle returns a sequence for setting the window title. +// +// OSC 2 ; title ST +// OSC 2 ; title BEL +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands +func SetWindowTitle(s string) string { + return "\x1b]2;" + s + "\x07" +} diff --git a/vendor/github.com/charmbracelet/x/ansi/truncate.go b/vendor/github.com/charmbracelet/x/ansi/truncate.go new file mode 100644 index 0000000..1fa3efe --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/truncate.go @@ -0,0 +1,282 @@ +package ansi + +import ( + "bytes" + + "github.com/charmbracelet/x/ansi/parser" + "github.com/mattn/go-runewidth" + "github.com/rivo/uniseg" +) + +// 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 + } + + tw := StringWidth(tail) + length -= tw + if length < 0 { + return "" + } + + var cluster []byte + var buf bytes.Buffer + curWidth := 0 + ignoring := false + pstate := parser.GroundState // initial state + b := []byte(s) + i := 0 + + // 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) { + state, action := parser.Table.Transition(pstate, b[i]) + if state == parser.Utf8State { + // 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) + + // Are we ignoring? Skip to the next byte + if ignoring { + continue + } + + // Is this gonna be too wide? + // If so write the tail and stop collecting. + if curWidth+width > length && !ignoring { + ignoring = true + buf.WriteString(tail) + } + + if curWidth+width > length { + continue + } + + curWidth += width + buf.Write(cluster) + + // Done collecting, now we're back in the ground state. + pstate = parser.GroundState + continue + } + + switch action { + case parser.PrintAction: + // Is this gonna be too wide? + // If so write the tail and stop collecting. + if curWidth >= length && !ignoring { + ignoring = true + buf.WriteString(tail) + } + + // Skip to the next byte if we're ignoring + if ignoring { + i++ + continue + } + + // collects printable ASCII + curWidth++ + fallthrough + default: + buf.WriteByte(b[i]) + i++ + } + + // Transition to the next state. + pstate = state + + // Once we reach the given length, we start ignoring runes and write + // the tail to the buffer. + if curWidth > length && !ignoring { + ignoring = true + buf.WriteString(tail) + } + } + + return buf.String() +} + +// 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-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 + var buf bytes.Buffer + curWidth := 0 + ignoring := true + pstate := parser.GroundState + b := []byte(s) + i := 0 + + for i < len(b) { + if !ignoring { + buf.Write(b[i:]) + break + } + + state, action := parser.Table.Transition(pstate, b[i]) + 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 > n && ignoring { + ignoring = false + buf.WriteString(prefix) + } + + if ignoring { + continue + } + + if curWidth > n { + buf.Write(cluster) + } + + pstate = parser.GroundState + continue + } + + switch action { + case parser.PrintAction: + curWidth++ + + if curWidth > n && ignoring { + ignoring = false + buf.WriteString(prefix) + } + + if ignoring { + i++ + continue + } + + fallthrough + default: + buf.WriteByte(b[i]) + i++ + } + + pstate = state + if curWidth > n && ignoring { + ignoring = false + buf.WriteString(prefix) + } + } + + 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 +} diff --git a/vendor/github.com/charmbracelet/x/ansi/util.go b/vendor/github.com/charmbracelet/x/ansi/util.go new file mode 100644 index 0000000..301ef15 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/util.go @@ -0,0 +1,106 @@ +package ansi + +import ( + "fmt" + "image/color" + "strconv" + "strings" + + "github.com/lucasb-eyer/go-colorful" +) + +// colorToHexString returns a hex string representation of a color. +func colorToHexString(c color.Color) string { + if c == nil { + return "" + } + shift := func(v uint32) uint32 { + if v > 0xff { + return v >> 8 + } + return v + } + r, g, b, _ := c.RGBA() + r, g, b = shift(r), shift(g), shift(b) + return fmt.Sprintf("#%02x%02x%02x", r, g, b) +} + +// rgbToHex converts red, green, and blue values to a hexadecimal value. +// +// hex := rgbToHex(0, 0, 255) // 0x0000FF +func rgbToHex(r, g, b uint32) uint32 { + return r<<16 + g<<8 + b +} + +type shiftable interface { + ~uint | ~uint16 | ~uint32 | ~uint64 +} + +func shift[T shiftable](x T) T { + if x > 0xff { + x >>= 8 + } + return x +} + +// XParseColor is a helper function that parses a string into a color.Color. It +// provides a similar interface to the XParseColor function in Xlib. It +// supports the following formats: +// +// - #RGB +// - #RRGGBB +// - rgb:RRRR/GGGG/BBBB +// - rgba:RRRR/GGGG/BBBB/AAAA +// +// If the string is not a valid color, nil is returned. +// +// See: https://linux.die.net/man/3/xparsecolor +func XParseColor(s string) color.Color { + switch { + case strings.HasPrefix(s, "#"): + c, err := colorful.Hex(s) + if err != nil { + return nil + } + + return c + case strings.HasPrefix(s, "rgb:"): + parts := strings.Split(s[4:], "/") + if len(parts) != 3 { + return nil + } + + r, _ := strconv.ParseUint(parts[0], 16, 32) + g, _ := strconv.ParseUint(parts[1], 16, 32) + b, _ := strconv.ParseUint(parts[2], 16, 32) + + return color.RGBA{uint8(shift(r)), uint8(shift(g)), uint8(shift(b)), 255} //nolint:gosec + case strings.HasPrefix(s, "rgba:"): + parts := strings.Split(s[5:], "/") + if len(parts) != 4 { + return nil + } + + r, _ := strconv.ParseUint(parts[0], 16, 32) + g, _ := strconv.ParseUint(parts[1], 16, 32) + b, _ := strconv.ParseUint(parts[2], 16, 32) + a, _ := strconv.ParseUint(parts[3], 16, 32) + + return color.RGBA{uint8(shift(r)), uint8(shift(g)), uint8(shift(b)), uint8(shift(a))} //nolint:gosec + } + 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 +} diff --git a/vendor/github.com/charmbracelet/x/ansi/width.go b/vendor/github.com/charmbracelet/x/ansi/width.go new file mode 100644 index 0000000..d0487d3 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/width.go @@ -0,0 +1,113 @@ +package ansi + +import ( + "bytes" + + "github.com/charmbracelet/x/ansi/parser" + "github.com/mattn/go-runewidth" + "github.com/rivo/uniseg" +) + +// Strip removes ANSI escape codes from a string. +func Strip(s string) string { + var ( + buf bytes.Buffer // buffer for collecting printable characters + ri int // rune index + rw int // rune width + pstate = parser.GroundState // initial state + ) + + // This implements a subset of the Parser to only collect runes and + // printable characters. + for i := 0; i < len(s); i++ { + if pstate == parser.Utf8State { + // During this state, collect rw bytes to form a valid rune in the + // buffer. After getting all the rune bytes into the buffer, + // transition to GroundState and reset the counters. + buf.WriteByte(s[i]) + ri++ + if ri < rw { + continue + } + pstate = parser.GroundState + ri = 0 + rw = 0 + continue + } + + state, action := parser.Table.Transition(pstate, s[i]) + switch action { + case parser.CollectAction: + if state == parser.Utf8State { + // This action happens when we transition to the Utf8State. + rw = utf8ByteLen(s[i]) + buf.WriteByte(s[i]) + ri++ + } + case parser.PrintAction, parser.ExecuteAction: + // collects printable ASCII and non-printable characters + buf.WriteByte(s[i]) + } + + // Transition to the next state. + // The Utf8State is managed separately above. + if pstate != parser.Utf8State { + pstate = state + } + } + + return buf.String() +} + +// 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. +// 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 + } + + var ( + pstate = parser.GroundState // initial state + cluster string + width int + ) + + for i := 0; i < len(s); i++ { + state, action := parser.Table.Transition(pstate, s[i]) + 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 + continue + } + + if action == parser.PrintAction { + width++ + } + + pstate = state + } + + return width +} diff --git a/vendor/github.com/charmbracelet/x/ansi/winop.go b/vendor/github.com/charmbracelet/x/ansi/winop.go new file mode 100644 index 0000000..0238780 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/winop.go @@ -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...) +} diff --git a/vendor/github.com/charmbracelet/x/ansi/wrap.go b/vendor/github.com/charmbracelet/x/ansi/wrap.go new file mode 100644 index 0000000..6b99580 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/wrap.go @@ -0,0 +1,467 @@ +package ansi + +import ( + "bytes" + "unicode" + "unicode/utf8" + + "github.com/charmbracelet/x/ansi/parser" + "github.com/mattn/go-runewidth" + "github.com/rivo/uniseg" +) + +// nbsp is a non-breaking space +const nbsp = 0xA0 + +// 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 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 + } + + var ( + cluster []byte + buf bytes.Buffer + curWidth int + forceNewline bool + pstate = parser.GroundState // initial state + b = []byte(s) + ) + + addNewline := func() { + buf.WriteByte('\n') + curWidth = 0 + } + + i := 0 + for i < len(b) { + state, action := parser.Table.Transition(pstate, b[i]) + 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 { + addNewline() + } + if !preserveSpace && curWidth == 0 && len(cluster) <= 4 { + // Skip spaces at the beginning of a line + if r, _ := utf8.DecodeRune(cluster); r != utf8.RuneError && unicode.IsSpace(r) { + pstate = parser.GroundState + continue + } + } + + buf.Write(cluster) + curWidth += width + pstate = parser.GroundState + continue + } + + switch action { + case parser.PrintAction, parser.ExecuteAction: + if b[i] == '\n' { + addNewline() + forceNewline = false + break + } + + if curWidth+1 > limit { + addNewline() + forceNewline = true + } + + // Skip spaces at the beginning of a line + if curWidth == 0 { + if !preserveSpace && forceNewline && unicode.IsSpace(rune(b[i])) { + break + } + forceNewline = false + } + + buf.WriteByte(b[i]) + if action == parser.PrintAction { + curWidth++ + } + default: + buf.WriteByte(b[i]) + } + + // We manage the UTF8 state separately manually above. + if pstate != parser.Utf8State { + pstate = state + } + i++ + } + + return buf.String() +} + +// 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. +// +// 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 + } + + var ( + cluster []byte + buf bytes.Buffer + word bytes.Buffer + space bytes.Buffer + curWidth int + wordLen int + pstate = parser.GroundState // initial state + b = []byte(s) + ) + + addSpace := func() { + curWidth += space.Len() + buf.Write(space.Bytes()) + space.Reset() + } + + addWord := func() { + if word.Len() == 0 { + return + } + + addSpace() + curWidth += wordLen + buf.Write(word.Bytes()) + word.Reset() + wordLen = 0 + } + + addNewline := func() { + buf.WriteByte('\n') + curWidth = 0 + space.Reset() + } + + i := 0 + for i < len(b) { + state, action := parser.Table.Transition(pstate, b[i]) + 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) + if r != utf8.RuneError && unicode.IsSpace(r) && r != nbsp { + addWord() + space.WriteRune(r) + } else if bytes.ContainsAny(cluster, breakpoints) { + addSpace() + addWord() + buf.Write(cluster) + curWidth++ + } else { + word.Write(cluster) + wordLen += width + if curWidth+space.Len()+wordLen > limit && + wordLen < limit { + addNewline() + } + } + + pstate = parser.GroundState + continue + } + + switch action { + case parser.PrintAction, parser.ExecuteAction: + r := rune(b[i]) + switch { + case r == '\n': + if wordLen == 0 { + if curWidth+space.Len() > limit { + curWidth = 0 + } else { + buf.Write(space.Bytes()) + } + space.Reset() + } + + addWord() + addNewline() + case unicode.IsSpace(r): + addWord() + space.WriteByte(b[i]) + case r == '-': + fallthrough + case runeContainsAny(r, breakpoints): + addSpace() + addWord() + buf.WriteByte(b[i]) + curWidth++ + default: + word.WriteByte(b[i]) + wordLen++ + if curWidth+space.Len()+wordLen > limit && + wordLen < limit { + addNewline() + } + } + + default: + word.WriteByte(b[i]) + } + + // We manage the UTF8 state separately manually above. + if pstate != parser.Utf8State { + pstate = state + } + i++ + } + + addWord() + + return buf.String() +} + +// 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. +// +// 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 + } + + var ( + cluster []byte + buf bytes.Buffer + word bytes.Buffer + space bytes.Buffer + curWidth int // written width of the line + wordLen int // word buffer len without ANSI escape codes + pstate = parser.GroundState // initial state + b = []byte(s) + ) + + addSpace := func() { + curWidth += space.Len() + buf.Write(space.Bytes()) + space.Reset() + } + + addWord := func() { + if word.Len() == 0 { + return + } + + addSpace() + curWidth += wordLen + buf.Write(word.Bytes()) + word.Reset() + wordLen = 0 + } + + addNewline := func() { + buf.WriteByte('\n') + curWidth = 0 + space.Reset() + } + + i := 0 + for i < len(b) { + state, action := parser.Table.Transition(pstate, b[i]) + 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) + switch { + case r != utf8.RuneError && unicode.IsSpace(r) && r != nbsp: // nbsp is a non-breaking space + addWord() + space.WriteRune(r) + case bytes.ContainsAny(cluster, breakpoints): + addSpace() + if curWidth+wordLen+width > limit { + word.Write(cluster) + wordLen += width + } else { + addWord() + buf.Write(cluster) + curWidth += width + } + default: + if wordLen+width > limit { + // Hardwrap the word if it's too long + addWord() + } + + word.Write(cluster) + wordLen += width + + if curWidth+wordLen+space.Len() > limit { + addNewline() + } + } + + pstate = parser.GroundState + continue + } + + switch action { + case parser.PrintAction, parser.ExecuteAction: + switch r := rune(b[i]); { + case r == '\n': + if wordLen == 0 { + if curWidth+space.Len() > limit { + curWidth = 0 + } else { + // preserve whitespaces + buf.Write(space.Bytes()) + } + space.Reset() + } + + addWord() + addNewline() + case unicode.IsSpace(r): + addWord() + space.WriteRune(r) + case r == '-': + fallthrough + case runeContainsAny(r, breakpoints): + addSpace() + if curWidth+wordLen >= limit { + // We can't fit the breakpoint in the current line, treat + // it as part of the word. + word.WriteRune(r) + wordLen++ + } else { + addWord() + buf.WriteRune(r) + curWidth++ + } + default: + if curWidth == limit { + addNewline() + } + word.WriteRune(r) + wordLen++ + + if wordLen == limit { + // Hardwrap the word if it's too long + addWord() + } + + if curWidth+wordLen+space.Len() > limit { + addNewline() + } + } + + default: + word.WriteByte(b[i]) + } + + // We manage the UTF8 state separately manually above. + if pstate != parser.Utf8State { + pstate = state + } + i++ + } + + if wordLen == 0 { + if curWidth+space.Len() > limit { + curWidth = 0 + } else { + // preserve whitespaces + buf.Write(space.Bytes()) + } + space.Reset() + } + + addWord() + + return buf.String() +} + +func runeContainsAny(r rune, s string) bool { + for _, c := range s { + if c == r { + return true + } + } + return false +} diff --git a/vendor/github.com/charmbracelet/x/ansi/xterm.go b/vendor/github.com/charmbracelet/x/ansi/xterm.go new file mode 100644 index 0000000..83fd4bd --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/xterm.go @@ -0,0 +1,138 @@ +package ansi + +import "strconv" + +// KeyModifierOptions (XTMODKEYS) sets/resets xterm key modifier options. +// +// Default is 0. +// +// CSI > Pp m +// CSI > Pp ; Pv m +// +// If Pv is omitted, the resource is reset to its initial value. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ +func KeyModifierOptions(p int, vs ...int) string { + var pp, pv string + if p > 0 { + pp = strconv.Itoa(p) + } + + if len(vs) == 0 { + return "\x1b[>" + strconv.Itoa(p) + "m" + } + + v := vs[0] + if v > 0 { + pv = strconv.Itoa(v) + return "\x1b[>" + pp + ";" + pv + "m" + } + + return "\x1b[>" + pp + "m" +} + +// XTMODKEYS is an alias for [KeyModifierOptions]. +func XTMODKEYS(p int, vs ...int) string { + return KeyModifierOptions(p, vs...) +} + +// SetKeyModifierOptions sets xterm key modifier options. +// This is an alias for [KeyModifierOptions]. +func SetKeyModifierOptions(pp int, pv int) string { + return KeyModifierOptions(pp, pv) +} + +// ResetKeyModifierOptions resets xterm key modifier options. +// This is an alias for [KeyModifierOptions]. +func ResetKeyModifierOptions(pp int) string { + return KeyModifierOptions(pp) +} + +// QueryKeyModifierOptions (XTQMODKEYS) requests xterm key modifier options. +// +// Default is 0. +// +// CSI ? Pp m +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ +func QueryKeyModifierOptions(pp int) string { + var p string + if pp > 0 { + p = strconv.Itoa(pp) + } + return "\x1b[?" + p + "m" +} + +// XTQMODKEYS is an alias for [QueryKeyModifierOptions]. +func XTQMODKEYS(pp int) string { + return QueryKeyModifierOptions(pp) +} + +// Modify Other Keys (modifyOtherKeys) is an xterm feature that allows the +// terminal to modify the behavior of certain keys to send different escape +// sequences when pressed. +// +// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys +const ( + SetModifyOtherKeys1 = "\x1b[>4;1m" + SetModifyOtherKeys2 = "\x1b[>4;2m" + ResetModifyOtherKeys = "\x1b[>4m" + QueryModifyOtherKeys = "\x1b[?4m" +) + +// ModifyOtherKeys returns a sequence that sets XTerm modifyOtherKeys mode. +// The mode argument specifies the mode to set. +// +// 0: Disable modifyOtherKeys mode. +// 1: Enable modifyOtherKeys mode 1. +// 2: Enable modifyOtherKeys mode 2. +// +// CSI > 4 ; mode m +// +// 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" +} + +// DisableModifyOtherKeys disables the modifyOtherKeys mode. +// +// CSI > 4 ; 0 m +// +// 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" + +// EnableModifyOtherKeys1 enables the modifyOtherKeys mode 1. +// +// CSI > 4 ; 1 m +// +// 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" + +// EnableModifyOtherKeys2 enables the modifyOtherKeys mode 2. +// +// CSI > 4 ; 2 m +// +// 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" + +// RequestModifyOtherKeys requests the modifyOtherKeys mode. +// +// CSI ? 4 m +// +// 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" diff --git a/vendor/github.com/charmbracelet/x/cellbuf/LICENSE b/vendor/github.com/charmbracelet/x/cellbuf/LICENSE new file mode 100644 index 0000000..65a5654 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/cellbuf/LICENSE @@ -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. diff --git a/vendor/github.com/charmbracelet/x/cellbuf/buffer.go b/vendor/github.com/charmbracelet/x/cellbuf/buffer.go new file mode 100644 index 0000000..790d1f7 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/cellbuf/buffer.go @@ -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) + } +} diff --git a/vendor/github.com/charmbracelet/x/cellbuf/cell.go b/vendor/github.com/charmbracelet/x/cellbuf/cell.go new file mode 100644 index 0000000..991c919 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/cellbuf/cell.go @@ -0,0 +1,503 @@ +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 +) + +// 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 +} diff --git a/vendor/github.com/charmbracelet/x/cellbuf/errors.go b/vendor/github.com/charmbracelet/x/cellbuf/errors.go new file mode 100644 index 0000000..64258fe --- /dev/null +++ b/vendor/github.com/charmbracelet/x/cellbuf/errors.go @@ -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") diff --git a/vendor/github.com/charmbracelet/x/cellbuf/geom.go b/vendor/github.com/charmbracelet/x/cellbuf/geom.go new file mode 100644 index 0000000..c12e6fb --- /dev/null +++ b/vendor/github.com/charmbracelet/x/cellbuf/geom.go @@ -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) +} diff --git a/vendor/github.com/charmbracelet/x/cellbuf/hardscroll.go b/vendor/github.com/charmbracelet/x/cellbuf/hardscroll.go new file mode 100644 index 0000000..402ac06 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/cellbuf/hardscroll.go @@ -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 +} diff --git a/vendor/github.com/charmbracelet/x/cellbuf/hashmap.go b/vendor/github.com/charmbracelet/x/cellbuf/hashmap.go new file mode 100644 index 0000000..0d25b54 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/cellbuf/hashmap.go @@ -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 +} diff --git a/vendor/github.com/charmbracelet/x/cellbuf/link.go b/vendor/github.com/charmbracelet/x/cellbuf/link.go new file mode 100644 index 0000000..112f8e8 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/cellbuf/link.go @@ -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 +} diff --git a/vendor/github.com/charmbracelet/x/cellbuf/screen.go b/vendor/github.com/charmbracelet/x/cellbuf/screen.go new file mode 100644 index 0000000..963b9ca --- /dev/null +++ b/vendor/github.com/charmbracelet/x/cellbuf/screen.go @@ -0,0 +1,1457 @@ +package cellbuf + +import ( + "bytes" + "errors" + "io" + "os" + "strings" + "sync" + + "github.com/charmbracelet/colorprofile" + "github.com/charmbracelet/x/ansi" + "github.com/charmbracelet/x/term" +) + +// ErrInvalidDimensions is returned when the dimensions of a window are invalid +// for the operation. +var ErrInvalidDimensions = errors.New("invalid dimensions") + +// notLocal returns whether the coordinates are not considered local movement +// using the defined thresholds. +// This takes the number of columns, and the coordinates of the current and +// target positions. +func notLocal(cols, fx, fy, tx, ty int) bool { + // The typical distance for a [ansi.CUP] sequence. Anything less than this + // is considered local movement. + const longDist = 8 - 1 + return (tx > longDist) && + (tx < cols-1-longDist) && + (abs(ty-fy)+abs(tx-fx) > longDist) +} + +// relativeCursorMove returns the relative cursor movement sequence using one or two +// of the following sequences [ansi.CUU], [ansi.CUD], [ansi.CUF], [ansi.CUB], +// [ansi.VPA], [ansi.HPA]. +// When overwrite is true, this will try to optimize the sequence by using the +// screen cells values to move the cursor instead of using escape sequences. +func relativeCursorMove(s *Screen, fx, fy, tx, ty int, overwrite, useTabs, useBackspace bool) string { + var seq strings.Builder + + width, height := s.newbuf.Width(), s.newbuf.Height() + if ty != fy { + var yseq string + if s.xtermLike && !s.opts.RelativeCursor { + yseq = ansi.VerticalPositionAbsolute(ty + 1) + } + + // OPTIM: Use [ansi.LF] and [ansi.ReverseIndex] as optimizations. + + if ty > fy { + n := ty - fy + if cud := ansi.CursorDown(n); yseq == "" || len(cud) < len(yseq) { + yseq = cud + } + shouldScroll := !s.opts.AltScreen && fy+n >= s.scrollHeight + if lf := strings.Repeat("\n", n); shouldScroll || (fy+n < height && len(lf) < len(yseq)) { + // TODO: Ensure we're not unintentionally scrolling the screen down. + yseq = lf + s.scrollHeight = max(s.scrollHeight, fy+n) + } + } else if ty < fy { + n := fy - ty + if cuu := ansi.CursorUp(n); yseq == "" || len(cuu) < len(yseq) { + yseq = cuu + } + if n == 1 && fy-1 > 0 { + // TODO: Ensure we're not unintentionally scrolling the screen up. + yseq = ansi.ReverseIndex + } + } + + seq.WriteString(yseq) + } + + if tx != fx { + var xseq string + if s.xtermLike && !s.opts.RelativeCursor { + xseq = ansi.HorizontalPositionAbsolute(tx + 1) + } + + if tx > fx { + n := tx - fx + if useTabs { + var tabs int + var col int + for col = fx; s.tabs.Next(col) <= tx; col = s.tabs.Next(col) { + tabs++ + if col == s.tabs.Next(col) || col >= width-1 { + break + } + } + + if tabs > 0 { + cht := ansi.CursorHorizontalForwardTab(tabs) + tab := strings.Repeat("\t", tabs) + if false && s.xtermLike && len(cht) < len(tab) { + // TODO: The linux console and some terminals such as + // Alacritty don't support [ansi.CHT]. Enable this when + // we have a way to detect this, or after 5 years when + // we're sure everyone has updated their terminals :P + seq.WriteString(cht) + } else { + seq.WriteString(tab) + } + + n = tx - col + fx = col + } + } + + if cuf := ansi.CursorForward(n); xseq == "" || len(cuf) < len(xseq) { + xseq = cuf + } + + // If we have no attribute and style changes, overwrite is cheaper. + var ovw string + if overwrite && ty >= 0 { + for i := 0; i < n; i++ { + cell := s.newbuf.Cell(fx+i, ty) + if cell != nil && cell.Width > 0 { + i += cell.Width - 1 + if !cell.Style.Equal(&s.cur.Style) || !cell.Link.Equal(&s.cur.Link) { + overwrite = false + break + } + } + } + } + + if overwrite && ty >= 0 { + for i := 0; i < n; i++ { + cell := s.newbuf.Cell(fx+i, ty) + if cell != nil && cell.Width > 0 { + ovw += cell.String() + i += cell.Width - 1 + } else { + ovw += " " + } + } + } + + if overwrite && len(ovw) < len(xseq) { + xseq = ovw + } + } else if tx < fx { + n := fx - tx + if useTabs && s.xtermLike { + // VT100 does not support backward tabs [ansi.CBT]. + + col := fx + + var cbt int // cursor backward tabs count + for s.tabs.Prev(col) >= tx { + col = s.tabs.Prev(col) + cbt++ + if col == s.tabs.Prev(col) || col <= 0 { + break + } + } + + if cbt > 0 { + seq.WriteString(ansi.CursorBackwardTab(cbt)) + n = col - tx + } + } + + if cub := ansi.CursorBackward(n); xseq == "" || len(cub) < len(xseq) { + xseq = cub + } + + if useBackspace && n < len(xseq) { + xseq = strings.Repeat("\b", n) + } + } + + seq.WriteString(xseq) + } + + return seq.String() +} + +// moveCursor moves and returns the cursor movement sequence to move the cursor +// to the specified position. +// When overwrite is true, this will try to optimize the sequence by using the +// screen cells values to move the cursor instead of using escape sequences. +func moveCursor(s *Screen, x, y int, overwrite bool) (seq string) { + fx, fy := s.cur.X, s.cur.Y + + if !s.opts.RelativeCursor { + // Method #0: Use [ansi.CUP] if the distance is long. + seq = ansi.CursorPosition(x+1, y+1) + if fx == -1 || fy == -1 || notLocal(s.newbuf.Width(), fx, fy, x, y) { + return + } + } + + // Optimize based on options. + trials := 0 + if s.opts.HardTabs { + trials |= 2 // 0b10 in binary + } + if s.opts.Backspace { + trials |= 1 // 0b01 in binary + } + + // Try all possible combinations of hard tabs and backspace optimizations. + for i := 0; i <= trials; i++ { + // Skip combinations that are not enabled. + if i & ^trials != 0 { + continue + } + + useHardTabs := i&2 != 0 + useBackspace := i&1 != 0 + + // Method #1: Use local movement sequences. + nseq := relativeCursorMove(s, fx, fy, x, y, overwrite, useHardTabs, useBackspace) + if (i == 0 && len(seq) == 0) || len(nseq) < len(seq) { + seq = nseq + } + + // Method #2: Use [ansi.CR] and local movement sequences. + nseq = "\r" + relativeCursorMove(s, 0, fy, x, y, overwrite, useHardTabs, useBackspace) + if len(nseq) < len(seq) { + seq = nseq + } + + if !s.opts.RelativeCursor { + // Method #3: Use [ansi.CursorHomePosition] and local movement sequences. + nseq = ansi.CursorHomePosition + relativeCursorMove(s, 0, 0, x, y, overwrite, useHardTabs, useBackspace) + if len(nseq) < len(seq) { + seq = nseq + } + } + } + + return +} + +// moveCursor moves the cursor to the specified position. +func (s *Screen) moveCursor(x, y int, overwrite bool) { + if !s.opts.AltScreen && s.cur.X == -1 && s.cur.Y == -1 { + // First cursor movement in inline mode, move the cursor to the first + // column before moving to the target position. + s.buf.WriteByte('\r') //nolint:errcheck + s.cur.X, s.cur.Y = 0, 0 + } + s.buf.WriteString(moveCursor(s, x, y, overwrite)) //nolint:errcheck + s.cur.X, s.cur.Y = x, y +} + +func (s *Screen) move(x, y int) { + // XXX: Make sure we use the max height and width of the buffer in case + // we're in the middle of a resize operation. + width := max(s.newbuf.Width(), s.curbuf.Width()) + height := max(s.newbuf.Height(), s.curbuf.Height()) + + if width > 0 && x >= width { + // Handle autowrap + y += (x / width) + x %= width + } + + // XXX: Disable styles if there's any + // Some move operations such as [ansi.LF] can apply styles to the new + // cursor position, thus, we need to reset the styles before moving the + // cursor. + blank := s.clearBlank() + resetPen := y != s.cur.Y && !blank.Equal(&BlankCell) + if resetPen { + s.updatePen(nil) + } + + // Reset wrap around (phantom cursor) state + if s.atPhantom { + s.cur.X = 0 + s.buf.WriteByte('\r') //nolint:errcheck + s.atPhantom = false // reset phantom cell state + } + + // TODO: Investigate if we need to handle this case and/or if we need the + // following code. + // + // if width > 0 && s.cur.X >= width { + // l := (s.cur.X + 1) / width + // + // s.cur.Y += l + // if height > 0 && s.cur.Y >= height { + // l -= s.cur.Y - height - 1 + // } + // + // if l > 0 { + // s.cur.X = 0 + // s.buf.WriteString("\r" + strings.Repeat("\n", l)) //nolint:errcheck + // } + // } + + if height > 0 { + if s.cur.Y > height-1 { + s.cur.Y = height - 1 + } + if y > height-1 { + y = height - 1 + } + } + + if x == s.cur.X && y == s.cur.Y { + // We give up later because we need to run checks for the phantom cell + // and others before we can determine if we can give up. + return + } + + // We set the new cursor in [Screen.moveCursor]. + s.moveCursor(x, y, true) // Overwrite cells if possible +} + +// Cursor represents a terminal Cursor. +type Cursor struct { + Style + Link + Position +} + +// ScreenOptions are options for the screen. +type ScreenOptions struct { + // Term is the terminal type to use when writing to the screen. When empty, + // `$TERM` is used from [os.Getenv]. + Term string + // Profile is the color profile to use when writing to the screen. + Profile colorprofile.Profile + // RelativeCursor is whether to use relative cursor movements. This is + // useful when alt-screen is not used or when using inline mode. + RelativeCursor bool + // AltScreen is whether to use the alternate screen buffer. + AltScreen bool + // ShowCursor is whether to show the cursor. + ShowCursor bool + // HardTabs is whether to use hard tabs to optimize cursor movements. + HardTabs bool + // Backspace is whether to use backspace characters to move the cursor. + Backspace bool +} + +// lineData represents the metadata for a line. +type lineData struct { + // first and last changed cell indices + firstCell, lastCell int + // old index used for scrolling + oldIndex int //nolint:unused +} + +// Screen represents the terminal screen. +type Screen struct { + w io.Writer + buf *bytes.Buffer // buffer for writing to the screen + curbuf *Buffer // the current buffer + newbuf *Buffer // the new buffer + tabs *TabStops + touch map[int]lineData + queueAbove []string // the queue of strings to write above the screen + oldhash, newhash []uint64 // the old and new hash values for each line + hashtab []hashmap // the hashmap table + oldnum []int // old indices from previous hash + cur, saved Cursor // the current and saved cursors + opts ScreenOptions + mu sync.Mutex + method ansi.Method + scrollHeight int // keeps track of how many lines we've scrolled down (inline mode) + altScreenMode bool // whether alternate screen mode is enabled + cursorHidden bool // whether text cursor mode is enabled + clear bool // whether to force clear the screen + xtermLike bool // whether to use xterm-like optimizations, otherwise, it uses vt100 only + queuedText bool // whether we have queued non-zero width text queued up + atPhantom bool // whether the cursor is out of bounds and at a phantom cell +} + +// SetMethod sets the method used to calculate the width of cells. +func (s *Screen) SetMethod(method ansi.Method) { + s.method = method +} + +// UseBackspaces sets whether to use backspace characters to move the cursor. +func (s *Screen) UseBackspaces(v bool) { + s.opts.Backspace = v +} + +// UseHardTabs sets whether to use hard tabs to optimize cursor movements. +func (s *Screen) UseHardTabs(v bool) { + s.opts.HardTabs = v +} + +// SetColorProfile sets the color profile to use when writing to the screen. +func (s *Screen) SetColorProfile(p colorprofile.Profile) { + s.opts.Profile = p +} + +// SetRelativeCursor sets whether to use relative cursor movements. +func (s *Screen) SetRelativeCursor(v bool) { + s.opts.RelativeCursor = v +} + +// EnterAltScreen enters the alternate screen buffer. +func (s *Screen) EnterAltScreen() { + s.opts.AltScreen = true + s.clear = true + s.saved = s.cur +} + +// ExitAltScreen exits the alternate screen buffer. +func (s *Screen) ExitAltScreen() { + s.opts.AltScreen = false + s.clear = true + s.cur = s.saved +} + +// ShowCursor shows the cursor. +func (s *Screen) ShowCursor() { + s.opts.ShowCursor = true +} + +// HideCursor hides the cursor. +func (s *Screen) HideCursor() { + s.opts.ShowCursor = false +} + +// Bounds implements Window. +func (s *Screen) Bounds() Rectangle { + // Always return the new buffer bounds. + return s.newbuf.Bounds() +} + +// Cell implements Window. +func (s *Screen) Cell(x int, y int) *Cell { + return s.newbuf.Cell(x, y) +} + +// Redraw forces a full redraw of the screen. +func (s *Screen) Redraw() { + s.mu.Lock() + s.clear = true + s.mu.Unlock() +} + +// Clear clears the screen with blank cells. This is a convenience method for +// [Screen.Fill] with a nil cell. +func (s *Screen) Clear() bool { + return s.ClearRect(s.newbuf.Bounds()) +} + +// ClearRect clears the given rectangle with blank cells. This is a convenience +// method for [Screen.FillRect] with a nil cell. +func (s *Screen) ClearRect(r Rectangle) bool { + return s.FillRect(nil, r) +} + +// SetCell implements Window. +func (s *Screen) SetCell(x int, y int, cell *Cell) (v bool) { + s.mu.Lock() + defer s.mu.Unlock() + cellWidth := 1 + if cell != nil { + cellWidth = cell.Width + } + if prev := s.curbuf.Cell(x, y); !cellEqual(prev, cell) { + chg, ok := s.touch[y] + if !ok { + chg = lineData{firstCell: x, lastCell: x + cellWidth} + } else { + chg.firstCell = min(chg.firstCell, x) + chg.lastCell = max(chg.lastCell, x+cellWidth) + } + s.touch[y] = chg + } + + return s.newbuf.SetCell(x, y, cell) +} + +// Fill implements Window. +func (s *Screen) Fill(cell *Cell) bool { + return s.FillRect(cell, s.newbuf.Bounds()) +} + +// FillRect implements Window. +func (s *Screen) FillRect(cell *Cell, r Rectangle) bool { + s.mu.Lock() + defer s.mu.Unlock() + s.newbuf.FillRect(cell, r) + for i := r.Min.Y; i < r.Max.Y; i++ { + s.touch[i] = lineData{firstCell: r.Min.X, lastCell: r.Max.X} + } + return true +} + +// isXtermLike returns whether the terminal is xterm-like. This means that the +// terminal supports ECMA-48 and ANSI X3.64 escape sequences. +// TODO: Should this be a lookup table into each $TERM terminfo database? Like +// we could keep a map of ANSI escape sequence to terminfo capability name and +// check if the database supports the escape sequence. Instead of keeping a +// list of terminal names here. +func isXtermLike(termtype string) (v bool) { + parts := strings.Split(termtype, "-") + if len(parts) == 0 { + return + } + + switch parts[0] { + case + "alacritty", + "contour", + "foot", + "ghostty", + "kitty", + "linux", + "rio", + "screen", + "st", + "tmux", + "wezterm", + "xterm": + v = true + } + + return +} + +// NewScreen creates a new Screen. +func NewScreen(w io.Writer, width, height int, opts *ScreenOptions) (s *Screen) { + s = new(Screen) + s.w = w + if opts != nil { + s.opts = *opts + } + + if s.opts.Term == "" { + s.opts.Term = os.Getenv("TERM") + } + + if width <= 0 || height <= 0 { + if f, ok := w.(term.File); ok { + width, height, _ = term.GetSize(f.Fd()) + } + } + if width < 0 { + width = 0 + } + if height < 0 { + height = 0 + } + + s.buf = new(bytes.Buffer) + s.xtermLike = isXtermLike(s.opts.Term) + s.curbuf = NewBuffer(width, height) + s.newbuf = NewBuffer(width, height) + s.cur = Cursor{Position: Pos(-1, -1)} // start at -1 to force a move + s.saved = s.cur + s.reset() + + return +} + +// Width returns the width of the screen. +func (s *Screen) Width() int { + return s.newbuf.Width() +} + +// Height returns the height of the screen. +func (s *Screen) Height() int { + return s.newbuf.Height() +} + +// cellEqual returns whether the two cells are equal. A nil cell is considered +// a [BlankCell]. +func cellEqual(a, b *Cell) bool { + if a == b { + return true + } + if a == nil { + a = &BlankCell + } + if b == nil { + b = &BlankCell + } + return a.Equal(b) +} + +// putCell draws a cell at the current cursor position. +func (s *Screen) putCell(cell *Cell) { + width, height := s.newbuf.Width(), s.newbuf.Height() + if s.opts.AltScreen && s.cur.X == width-1 && s.cur.Y == height-1 { + s.putCellLR(cell) + } else { + s.putAttrCell(cell) + } +} + +// wrapCursor wraps the cursor to the next line. +// +//nolint:unused +func (s *Screen) wrapCursor() { + const autoRightMargin = true + if autoRightMargin { + // Assume we have auto wrap mode enabled. + s.cur.X = 0 + s.cur.Y++ + } else { + s.cur.X-- + } +} + +func (s *Screen) putAttrCell(cell *Cell) { + if cell != nil && cell.Empty() { + // XXX: Zero width cells are special and should not be written to the + // screen no matter what other attributes they have. + // Zero width cells are used for wide characters that are split into + // multiple cells. + return + } + + if cell == nil { + cell = s.clearBlank() + } + + // We're at pending wrap state (phantom cell), incoming cell should + // wrap. + if s.atPhantom { + s.wrapCursor() + s.atPhantom = false + } + + s.updatePen(cell) + s.buf.WriteRune(cell.Rune) //nolint:errcheck + for _, c := range cell.Comb { + s.buf.WriteRune(c) //nolint:errcheck + } + + s.cur.X += cell.Width + + if cell.Width > 0 { + s.queuedText = true + } + + if s.cur.X >= s.newbuf.Width() { + s.atPhantom = true + } +} + +// putCellLR draws a cell at the lower right corner of the screen. +func (s *Screen) putCellLR(cell *Cell) { + // Optimize for the lower right corner cell. + curX := s.cur.X + if cell == nil || !cell.Empty() { + s.buf.WriteString(ansi.ResetAutoWrapMode) //nolint:errcheck + s.putAttrCell(cell) + // Writing to lower-right corner cell should not wrap. + s.atPhantom = false + s.cur.X = curX + s.buf.WriteString(ansi.SetAutoWrapMode) //nolint:errcheck + } +} + +// updatePen updates the cursor pen styles. +func (s *Screen) updatePen(cell *Cell) { + if cell == nil { + cell = &BlankCell + } + + if s.opts.Profile != 0 { + // Downsample colors to the given color profile. + cell.Style = ConvertStyle(cell.Style, s.opts.Profile) + cell.Link = ConvertLink(cell.Link, s.opts.Profile) + } + + if !cell.Style.Equal(&s.cur.Style) { + seq := cell.Style.DiffSequence(s.cur.Style) + if cell.Style.Empty() && len(seq) > len(ansi.ResetStyle) { + seq = ansi.ResetStyle + } + s.buf.WriteString(seq) //nolint:errcheck + s.cur.Style = cell.Style + } + if !cell.Link.Equal(&s.cur.Link) { + s.buf.WriteString(ansi.SetHyperlink(cell.Link.URL, cell.Link.Params)) //nolint:errcheck + s.cur.Link = cell.Link + } +} + +// emitRange emits a range of cells to the buffer. It it equivalent to calling +// [Screen.putCell] for each cell in the range. This is optimized to use +// [ansi.ECH] and [ansi.REP]. +// Returns whether the cursor is at the end of interval or somewhere in the +// middle. +func (s *Screen) emitRange(line Line, n int) (eoi bool) { + for n > 0 { + var count int + for n > 1 && !cellEqual(line.At(0), line.At(1)) { + s.putCell(line.At(0)) + line = line[1:] + n-- + } + + cell0 := line[0] + if n == 1 { + s.putCell(cell0) + return false + } + + count = 2 + for count < n && cellEqual(line.At(count), cell0) { + count++ + } + + ech := ansi.EraseCharacter(count) + cup := ansi.CursorPosition(s.cur.X+count, s.cur.Y) + rep := ansi.RepeatPreviousCharacter(count) + if s.xtermLike && count > len(ech)+len(cup) && cell0 != nil && cell0.Clear() { + s.updatePen(cell0) + s.buf.WriteString(ech) //nolint:errcheck + + // If this is the last cell, we don't need to move the cursor. + if count < n { + s.move(s.cur.X+count, s.cur.Y) + } else { + return true // cursor in the middle + } + } else if s.xtermLike && count > len(rep) && + (cell0 == nil || (len(cell0.Comb) == 0 && cell0.Rune < 256)) { + // We only support ASCII characters. Most terminals will handle + // non-ASCII characters correctly, but some might not, ahem xterm. + // + // NOTE: [ansi.REP] only repeats the last rune and won't work + // if the last cell contains multiple runes. + + wrapPossible := s.cur.X+count >= s.newbuf.Width() + repCount := count + if wrapPossible { + repCount-- + } + + s.updatePen(cell0) + s.putCell(cell0) + repCount-- // cell0 is a single width cell ASCII character + + s.buf.WriteString(ansi.RepeatPreviousCharacter(repCount)) //nolint:errcheck + s.cur.X += repCount + if wrapPossible { + s.putCell(cell0) + } + } else { + for i := 0; i < count; i++ { + s.putCell(line.At(i)) + } + } + + line = line[clamp(count, 0, len(line)):] + n -= count + } + + return +} + +// putRange puts a range of cells from the old line to the new line. +// Returns whether the cursor is at the end of interval or somewhere in the +// middle. +func (s *Screen) putRange(oldLine, newLine Line, y, start, end int) (eoi bool) { + inline := min(len(ansi.CursorPosition(start+1, y+1)), + min(len(ansi.HorizontalPositionAbsolute(start+1)), + len(ansi.CursorForward(start+1)))) + if (end - start + 1) > inline { + var j, same int + for j, same = start, 0; j <= end; j++ { + oldCell, newCell := oldLine.At(j), newLine.At(j) + if same == 0 && oldCell != nil && oldCell.Empty() { + continue + } + if cellEqual(oldCell, newCell) { + same++ + } else { + if same > end-start { + s.emitRange(newLine[start:], j-same-start) + s.move(j, y) + start = j + } + same = 0 + } + } + + i := s.emitRange(newLine[start:], j-same-start) + + // Always return 1 for the next [Screen.move] after a [Screen.putRange] if + // we found identical characters at end of interval. + if same == 0 { + return i + } + return true + } + + return s.emitRange(newLine[start:], end-start+1) +} + +// clearToEnd clears the screen from the current cursor position to the end of +// line. +func (s *Screen) clearToEnd(blank *Cell, force bool) { //nolint:unparam + if s.cur.Y >= 0 { + curline := s.curbuf.Line(s.cur.Y) + for j := s.cur.X; j < s.curbuf.Width(); j++ { + if j >= 0 { + c := curline.At(j) + if !cellEqual(c, blank) { + curline.Set(j, blank) + force = true + } + } + } + } + + if force { + s.updatePen(blank) + count := s.newbuf.Width() - s.cur.X + if s.el0Cost() <= count { + s.buf.WriteString(ansi.EraseLineRight) //nolint:errcheck + } else { + for i := 0; i < count; i++ { + s.putCell(blank) + } + } + } +} + +// clearBlank returns a blank cell based on the current cursor background color. +func (s *Screen) clearBlank() *Cell { + c := BlankCell + if !s.cur.Style.Empty() || !s.cur.Link.Empty() { + c.Style = s.cur.Style + c.Link = s.cur.Link + } + return &c +} + +// insertCells inserts the count cells pointed by the given line at the current +// cursor position. +func (s *Screen) insertCells(line Line, count int) { + if s.xtermLike { + // Use [ansi.ICH] as an optimization. + s.buf.WriteString(ansi.InsertCharacter(count)) //nolint:errcheck + } else { + // Otherwise, use [ansi.IRM] mode. + s.buf.WriteString(ansi.SetInsertReplaceMode) //nolint:errcheck + } + + for i := 0; count > 0; i++ { + s.putAttrCell(line[i]) + count-- + } + + if !s.xtermLike { + s.buf.WriteString(ansi.ResetInsertReplaceMode) //nolint:errcheck + } +} + +// el0Cost returns the cost of using [ansi.EL] 0 i.e. [ansi.EraseLineRight]. If +// this terminal supports background color erase, it can be cheaper to use +// [ansi.EL] 0 i.e. [ansi.EraseLineRight] to clear +// trailing spaces. +func (s *Screen) el0Cost() int { + if s.xtermLike { + return 0 + } + return len(ansi.EraseLineRight) +} + +// transformLine transforms the given line in the current window to the +// corresponding line in the new window. It uses [ansi.ICH] and [ansi.DCH] to +// insert or delete characters. +func (s *Screen) transformLine(y int) { + var firstCell, oLastCell, nLastCell int // first, old last, new last index + oldLine := s.curbuf.Line(y) + newLine := s.newbuf.Line(y) + + // Find the first changed cell in the line + var lineChanged bool + for i := 0; i < s.newbuf.Width(); i++ { + if !cellEqual(newLine.At(i), oldLine.At(i)) { + lineChanged = true + break + } + } + + const ceolStandoutGlitch = false + if ceolStandoutGlitch && lineChanged { + s.move(0, y) + s.clearToEnd(nil, false) + s.putRange(oldLine, newLine, y, 0, s.newbuf.Width()-1) + } else { + blank := newLine.At(0) + + // It might be cheaper to clear leading spaces with [ansi.EL] 1 i.e. + // [ansi.EraseLineLeft]. + if blank == nil || blank.Clear() { + var oFirstCell, nFirstCell int + for oFirstCell = 0; oFirstCell < s.curbuf.Width(); oFirstCell++ { + if !cellEqual(oldLine.At(oFirstCell), blank) { + break + } + } + for nFirstCell = 0; nFirstCell < s.newbuf.Width(); nFirstCell++ { + if !cellEqual(newLine.At(nFirstCell), blank) { + break + } + } + + if nFirstCell == oFirstCell { + firstCell = nFirstCell + + // Find the first differing cell + for firstCell < s.newbuf.Width() && + cellEqual(oldLine.At(firstCell), newLine.At(firstCell)) { + firstCell++ + } + } else if oFirstCell > nFirstCell { + firstCell = nFirstCell + } else if oFirstCell < nFirstCell { + firstCell = oFirstCell + el1Cost := len(ansi.EraseLineLeft) + if el1Cost < nFirstCell-oFirstCell { + if nFirstCell >= s.newbuf.Width() { + s.move(0, y) + s.updatePen(blank) + s.buf.WriteString(ansi.EraseLineRight) //nolint:errcheck + } else { + s.move(nFirstCell-1, y) + s.updatePen(blank) + s.buf.WriteString(ansi.EraseLineLeft) //nolint:errcheck + } + + for firstCell < nFirstCell { + oldLine.Set(firstCell, blank) + firstCell++ + } + } + } + } else { + // Find the first differing cell + for firstCell < s.newbuf.Width() && cellEqual(newLine.At(firstCell), oldLine.At(firstCell)) { + firstCell++ + } + } + + // If we didn't find one, we're done + if firstCell >= s.newbuf.Width() { + return + } + + blank = newLine.At(s.newbuf.Width() - 1) + if blank != nil && !blank.Clear() { + // Find the last differing cell + nLastCell = s.newbuf.Width() - 1 + for nLastCell > firstCell && cellEqual(newLine.At(nLastCell), oldLine.At(nLastCell)) { + nLastCell-- + } + + if nLastCell >= firstCell { + s.move(firstCell, y) + s.putRange(oldLine, newLine, y, firstCell, nLastCell) + if firstCell < len(oldLine) && firstCell < len(newLine) { + copy(oldLine[firstCell:], newLine[firstCell:]) + } else { + copy(oldLine, newLine) + } + } + + return + } + + // Find last non-blank cell in the old line. + oLastCell = s.curbuf.Width() - 1 + for oLastCell > firstCell && cellEqual(oldLine.At(oLastCell), blank) { + oLastCell-- + } + + // Find last non-blank cell in the new line. + nLastCell = s.newbuf.Width() - 1 + for nLastCell > firstCell && cellEqual(newLine.At(nLastCell), blank) { + nLastCell-- + } + + if nLastCell == firstCell && s.el0Cost() < oLastCell-nLastCell { + s.move(firstCell, y) + if !cellEqual(newLine.At(firstCell), blank) { + s.putCell(newLine.At(firstCell)) + } + s.clearToEnd(blank, false) + } else if nLastCell != oLastCell && + !cellEqual(newLine.At(nLastCell), oldLine.At(oLastCell)) { + s.move(firstCell, y) + if oLastCell-nLastCell > s.el0Cost() { + if s.putRange(oldLine, newLine, y, firstCell, nLastCell) { + s.move(nLastCell+1, y) + } + s.clearToEnd(blank, false) + } else { + n := max(nLastCell, oLastCell) + s.putRange(oldLine, newLine, y, firstCell, n) + } + } else { + nLastNonBlank := nLastCell + oLastNonBlank := oLastCell + + // Find the last cells that really differ. + // Can be -1 if no cells differ. + for cellEqual(newLine.At(nLastCell), oldLine.At(oLastCell)) { + if !cellEqual(newLine.At(nLastCell-1), oldLine.At(oLastCell-1)) { + break + } + nLastCell-- + oLastCell-- + if nLastCell == -1 || oLastCell == -1 { + break + } + } + + n := min(oLastCell, nLastCell) + if n >= firstCell { + s.move(firstCell, y) + s.putRange(oldLine, newLine, y, firstCell, n) + } + + if oLastCell < nLastCell { + m := max(nLastNonBlank, oLastNonBlank) + if n != 0 { + for n > 0 { + wide := newLine.At(n + 1) + if wide == nil || !wide.Empty() { + break + } + n-- + oLastCell-- + } + } else if n >= firstCell && newLine.At(n) != nil && newLine.At(n).Width > 1 { + next := newLine.At(n + 1) + for next != nil && next.Empty() { + n++ + oLastCell++ + } + } + + s.move(n+1, y) + ichCost := 3 + nLastCell - oLastCell + if s.xtermLike && (nLastCell < nLastNonBlank || ichCost > (m-n)) { + s.putRange(oldLine, newLine, y, n+1, m) + } else { + s.insertCells(newLine[n+1:], nLastCell-oLastCell) + } + } else if oLastCell > nLastCell { + s.move(n+1, y) + dchCost := 3 + oLastCell - nLastCell + if dchCost > len(ansi.EraseLineRight)+nLastNonBlank-(n+1) { + if s.putRange(oldLine, newLine, y, n+1, nLastNonBlank) { + s.move(nLastNonBlank+1, y) + } + s.clearToEnd(blank, false) + } else { + s.updatePen(blank) + s.deleteCells(oLastCell - nLastCell) + } + } + } + } + + // Update the old line with the new line + if firstCell < len(oldLine) && firstCell < len(newLine) { + copy(oldLine[firstCell:], newLine[firstCell:]) + } else { + copy(oldLine, newLine) + } +} + +// deleteCells deletes the count cells at the current cursor position and moves +// the rest of the line to the left. This is equivalent to [ansi.DCH]. +func (s *Screen) deleteCells(count int) { + // [ansi.DCH] will shift in cells from the right margin so we need to + // ensure that they are the right style. + s.buf.WriteString(ansi.DeleteCharacter(count)) //nolint:errcheck +} + +// clearToBottom clears the screen from the current cursor position to the end +// of the screen. +func (s *Screen) clearToBottom(blank *Cell) { + row, col := s.cur.Y, s.cur.X + if row < 0 { + row = 0 + } + + s.updatePen(blank) + s.buf.WriteString(ansi.EraseScreenBelow) //nolint:errcheck + // Clear the rest of the current line + s.curbuf.ClearRect(Rect(col, row, s.curbuf.Width()-col, 1)) + // Clear everything below the current line + s.curbuf.ClearRect(Rect(0, row+1, s.curbuf.Width(), s.curbuf.Height()-row-1)) +} + +// clearBottom tests if clearing the end of the screen would satisfy part of +// the screen update. Scan backwards through lines in the screen checking if +// each is blank and one or more are changed. +// It returns the top line. +func (s *Screen) clearBottom(total int) (top int) { + if total <= 0 { + return + } + + top = total + last := s.newbuf.Width() + blank := s.clearBlank() + canClearWithBlank := blank == nil || blank.Clear() + + if canClearWithBlank { + var row int + for row = total - 1; row >= 0; row-- { + oldLine := s.curbuf.Line(row) + newLine := s.newbuf.Line(row) + + var col int + ok := true + for col = 0; ok && col < last; col++ { + ok = cellEqual(newLine.At(col), blank) + } + if !ok { + break + } + + for col = 0; ok && col < last; col++ { + ok = len(oldLine) == last && cellEqual(oldLine.At(col), blank) + } + if !ok { + top = row + } + } + + if top < total { + s.move(0, top-1) // top is 1-based + s.clearToBottom(blank) + if s.oldhash != nil && s.newhash != nil && + row < len(s.oldhash) && row < len(s.newhash) { + for row := top; row < s.newbuf.Height(); row++ { + s.oldhash[row] = s.newhash[row] + } + } + } + } + + return +} + +// clearScreen clears the screen and put cursor at home. +func (s *Screen) clearScreen(blank *Cell) { + s.updatePen(blank) + s.buf.WriteString(ansi.CursorHomePosition) //nolint:errcheck + s.buf.WriteString(ansi.EraseEntireScreen) //nolint:errcheck + s.cur.X, s.cur.Y = 0, 0 + s.curbuf.Fill(blank) +} + +// clearBelow clears everything below and including the row. +func (s *Screen) clearBelow(blank *Cell, row int) { + s.move(0, row) + s.clearToBottom(blank) +} + +// clearUpdate forces a screen redraw. +func (s *Screen) clearUpdate() { + blank := s.clearBlank() + var nonEmpty int + if s.opts.AltScreen { + // XXX: We're using the maximum height of the two buffers to ensure + // we write newly added lines to the screen in [Screen.transformLine]. + nonEmpty = max(s.curbuf.Height(), s.newbuf.Height()) + s.clearScreen(blank) + } else { + nonEmpty = s.newbuf.Height() + s.clearBelow(blank, 0) + } + nonEmpty = s.clearBottom(nonEmpty) + for i := 0; i < nonEmpty; i++ { + s.transformLine(i) + } +} + +// Flush flushes the buffer to the screen. +func (s *Screen) Flush() (err error) { + s.mu.Lock() + defer s.mu.Unlock() + return s.flush() +} + +func (s *Screen) flush() (err error) { + // Write the buffer + if s.buf.Len() > 0 { + _, err = s.w.Write(s.buf.Bytes()) //nolint:errcheck + if err == nil { + s.buf.Reset() + } + } + + return +} + +// Render renders changes of the screen to the internal buffer. Call +// [Screen.Flush] to flush pending changes to the screen. +func (s *Screen) Render() { + s.mu.Lock() + s.render() + s.mu.Unlock() +} + +func (s *Screen) render() { + // Do we need to render anything? + if s.opts.AltScreen == s.altScreenMode && + !s.opts.ShowCursor == s.cursorHidden && + !s.clear && + len(s.touch) == 0 && + len(s.queueAbove) == 0 { + return + } + + // TODO: Investigate whether this is necessary. Theoretically, terminals + // can add/remove tab stops and we should be able to handle that. We could + // use [ansi.DECTABSR] to read the tab stops, but that's not implemented in + // most terminals :/ + // // Are we using hard tabs? If so, ensure tabs are using the + // // default interval using [ansi.DECST8C]. + // if s.opts.HardTabs && !s.initTabs { + // s.buf.WriteString(ansi.SetTabEvery8Columns) + // s.initTabs = true + // } + + // Do we need alt-screen mode? + if s.opts.AltScreen != s.altScreenMode { + if s.opts.AltScreen { + s.buf.WriteString(ansi.SetAltScreenSaveCursorMode) + } else { + s.buf.WriteString(ansi.ResetAltScreenSaveCursorMode) + } + s.altScreenMode = s.opts.AltScreen + } + + // Do we need text cursor mode? + if !s.opts.ShowCursor != s.cursorHidden { + s.cursorHidden = !s.opts.ShowCursor + if s.cursorHidden { + s.buf.WriteString(ansi.HideCursor) + } + } + + // Do we have queued strings to write above the screen? + if len(s.queueAbove) > 0 { + // TODO: Use scrolling region if available. + // TODO: Use [Screen.Write] [io.Writer] interface. + + // We need to scroll the screen up by the number of lines in the queue. + // We can't use [ansi.SU] because we want the cursor to move down until + // it reaches the bottom of the screen. + s.move(0, s.newbuf.Height()-1) + s.buf.WriteString(strings.Repeat("\n", len(s.queueAbove))) + s.cur.Y += len(s.queueAbove) + // XXX: Now go to the top of the screen, insert new lines, and write + // the queued strings. It is important to use [Screen.moveCursor] + // instead of [Screen.move] because we don't want to perform any checks + // on the cursor position. + s.moveCursor(0, 0, false) + s.buf.WriteString(ansi.InsertLine(len(s.queueAbove))) + for _, line := range s.queueAbove { + s.buf.WriteString(line + "\r\n") + } + + // Clear the queue + s.queueAbove = s.queueAbove[:0] + } + + var nonEmpty int + + // XXX: In inline mode, after a screen resize, we need to clear the extra + // lines at the bottom of the screen. This is because in inline mode, we + // don't use the full screen height and the current buffer size might be + // larger than the new buffer size. + partialClear := !s.opts.AltScreen && s.cur.X != -1 && s.cur.Y != -1 && + s.curbuf.Width() == s.newbuf.Width() && + s.curbuf.Height() > 0 && + s.curbuf.Height() > s.newbuf.Height() + + if !s.clear && partialClear { + s.clearBelow(nil, s.newbuf.Height()-1) + } + + if s.clear { + s.clearUpdate() + s.clear = false + } else if len(s.touch) > 0 { + if s.opts.AltScreen { + // Optimize scrolling for the alternate screen buffer. + // TODO: Should we optimize for inline mode as well? If so, we need + // to know the actual cursor position to use [ansi.DECSTBM]. + s.scrollOptimize() + } + + var changedLines int + var i int + + if s.opts.AltScreen { + nonEmpty = min(s.curbuf.Height(), s.newbuf.Height()) + } else { + nonEmpty = s.newbuf.Height() + } + + nonEmpty = s.clearBottom(nonEmpty) + for i = 0; i < nonEmpty; i++ { + _, ok := s.touch[i] + if ok { + s.transformLine(i) + changedLines++ + } + } + } + + // Sync windows and screen + s.touch = make(map[int]lineData, s.newbuf.Height()) + + if s.curbuf.Width() != s.newbuf.Width() || s.curbuf.Height() != s.newbuf.Height() { + // Resize the old buffer to match the new buffer. + _, oldh := s.curbuf.Width(), s.curbuf.Height() + s.curbuf.Resize(s.newbuf.Width(), s.newbuf.Height()) + // Sync new lines to old lines + for i := oldh - 1; i < s.newbuf.Height(); i++ { + copy(s.curbuf.Line(i), s.newbuf.Line(i)) + } + } + + s.updatePen(nil) // nil indicates a blank cell with no styles + + // Do we have enough changes to justify toggling the cursor? + if s.buf.Len() > 1 && s.opts.ShowCursor && !s.cursorHidden && s.queuedText { + nb := new(bytes.Buffer) + nb.Grow(s.buf.Len() + len(ansi.HideCursor) + len(ansi.ShowCursor)) + nb.WriteString(ansi.HideCursor) + nb.Write(s.buf.Bytes()) + nb.WriteString(ansi.ShowCursor) + *s.buf = *nb + } + + s.queuedText = false +} + +// Close writes the final screen update and resets the screen. +func (s *Screen) Close() (err error) { + s.mu.Lock() + defer s.mu.Unlock() + + s.render() + s.updatePen(nil) + // Go to the bottom of the screen + s.move(0, s.newbuf.Height()-1) + + if s.altScreenMode { + s.buf.WriteString(ansi.ResetAltScreenSaveCursorMode) + s.altScreenMode = false + } + + if s.cursorHidden { + s.buf.WriteString(ansi.ShowCursor) + s.cursorHidden = false + } + + // Write the buffer + err = s.flush() + if err != nil { + return + } + + s.reset() + return +} + +// reset resets the screen to its initial state. +func (s *Screen) reset() { + s.scrollHeight = 0 + s.cursorHidden = false + s.altScreenMode = false + s.touch = make(map[int]lineData, s.newbuf.Height()) + if s.curbuf != nil { + s.curbuf.Clear() + } + if s.newbuf != nil { + s.newbuf.Clear() + } + s.buf.Reset() + s.tabs = DefaultTabStops(s.newbuf.Width()) + s.oldhash, s.newhash = nil, nil + + // We always disable HardTabs when termtype is "linux". + if strings.HasPrefix(s.opts.Term, "linux") { + s.opts.HardTabs = false + } +} + +// Resize resizes the screen. +func (s *Screen) Resize(width, height int) bool { + oldw := s.newbuf.Width() + oldh := s.newbuf.Height() + + if s.opts.AltScreen || width != oldw { + // We only clear the whole screen if the width changes. Adding/removing + // rows is handled by the [Screen.render] and [Screen.transformLine] + // methods. + s.clear = true + } + + // Clear new columns and lines + if width > oldh { + s.ClearRect(Rect(max(oldw-1, 0), 0, width-oldw, height)) + } else if width < oldw { + s.ClearRect(Rect(max(width-1, 0), 0, oldw-width, height)) + } + + if height > oldh { + s.ClearRect(Rect(0, max(oldh-1, 0), width, height-oldh)) + } else if height < oldh { + s.ClearRect(Rect(0, max(height-1, 0), width, oldh-height)) + } + + s.mu.Lock() + s.newbuf.Resize(width, height) + s.tabs.Resize(width) + s.oldhash, s.newhash = nil, nil + s.scrollHeight = 0 // reset scroll lines + s.mu.Unlock() + + return true +} + +// MoveTo moves the cursor to the given position. +func (s *Screen) MoveTo(x, y int) { + s.mu.Lock() + s.move(x, y) + s.mu.Unlock() +} + +// InsertAbove inserts string above the screen. The inserted string is not +// managed by the screen. This does nothing when alternate screen mode is +// enabled. +func (s *Screen) InsertAbove(str string) { + if s.opts.AltScreen { + return + } + s.mu.Lock() + for _, line := range strings.Split(str, "\n") { + s.queueAbove = append(s.queueAbove, s.method.Truncate(line, s.Width(), "")) + } + s.mu.Unlock() +} diff --git a/vendor/github.com/charmbracelet/x/cellbuf/sequence.go b/vendor/github.com/charmbracelet/x/cellbuf/sequence.go new file mode 100644 index 0000000..613eefe --- /dev/null +++ b/vendor/github.com/charmbracelet/x/cellbuf/sequence.go @@ -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) +} diff --git a/vendor/github.com/charmbracelet/x/cellbuf/style.go b/vendor/github.com/charmbracelet/x/cellbuf/style.go new file mode 100644 index 0000000..82c4afb --- /dev/null +++ b/vendor/github.com/charmbracelet/x/cellbuf/style.go @@ -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 +} diff --git a/vendor/github.com/charmbracelet/x/cellbuf/tabstop.go b/vendor/github.com/charmbracelet/x/cellbuf/tabstop.go new file mode 100644 index 0000000..24eec44 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/cellbuf/tabstop.go @@ -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) + } + } +} diff --git a/vendor/github.com/charmbracelet/x/cellbuf/utils.go b/vendor/github.com/charmbracelet/x/cellbuf/utils.go new file mode 100644 index 0000000..b0452fa --- /dev/null +++ b/vendor/github.com/charmbracelet/x/cellbuf/utils.go @@ -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 +} diff --git a/vendor/github.com/charmbracelet/x/cellbuf/wrap.go b/vendor/github.com/charmbracelet/x/cellbuf/wrap.go new file mode 100644 index 0000000..59a2a33 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/cellbuf/wrap.go @@ -0,0 +1,178 @@ +package cellbuf + +import ( + "bytes" + "unicode" + "unicode/utf8" + + "github.com/charmbracelet/x/ansi" +) + +// 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 + ) + + 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 unicode.IsSpace(r) { + 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 +} diff --git a/vendor/github.com/charmbracelet/x/cellbuf/writer.go b/vendor/github.com/charmbracelet/x/cellbuf/writer.go new file mode 100644 index 0000000..ae8b2a8 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/cellbuf/writer.go @@ -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() + } +} diff --git a/vendor/github.com/charmbracelet/x/term/LICENSE b/vendor/github.com/charmbracelet/x/term/LICENSE new file mode 100644 index 0000000..65a5654 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/term/LICENSE @@ -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. diff --git a/vendor/github.com/charmbracelet/x/term/term.go b/vendor/github.com/charmbracelet/x/term/term.go new file mode 100644 index 0000000..58d6522 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/term/term.go @@ -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) +} diff --git a/vendor/github.com/charmbracelet/x/term/term_other.go b/vendor/github.com/charmbracelet/x/term/term_other.go new file mode 100644 index 0000000..092c7e9 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/term/term_other.go @@ -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) +} diff --git a/vendor/github.com/charmbracelet/x/term/term_unix.go b/vendor/github.com/charmbracelet/x/term/term_unix.go new file mode 100644 index 0000000..1459cb1 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/term/term_unix.go @@ -0,0 +1,96 @@ +//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos + +package term + +import ( + "golang.org/x/sys/unix" +) + +type state struct { + unix.Termios +} + +func isTerminal(fd uintptr) bool { + _, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios) + return err == nil +} + +func makeRaw(fd uintptr) (*State, error) { + termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios) + if err != nil { + return nil, err + } + + oldState := State{state{Termios: *termios}} + + // This attempts to replicate the behaviour documented for cfmakeraw in + // the termios(3) manpage. + termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON + termios.Oflag &^= unix.OPOST + termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN + termios.Cflag &^= unix.CSIZE | unix.PARENB + termios.Cflag |= unix.CS8 + termios.Cc[unix.VMIN] = 1 + termios.Cc[unix.VTIME] = 0 + if err := unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios); err != nil { + return nil, err + } + + return &oldState, nil +} + +func setState(fd uintptr, state *State) error { + var termios *unix.Termios + if state != nil { + termios = &state.Termios + } + return unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios) +} + +func getState(fd uintptr) (*State, error) { + termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios) + if err != nil { + return nil, err + } + + return &State{state{Termios: *termios}}, nil +} + +func restore(fd uintptr, state *State) error { + return unix.IoctlSetTermios(int(fd), ioctlWriteTermios, &state.Termios) +} + +func getSize(fd uintptr) (width, height int, err error) { + ws, err := unix.IoctlGetWinsize(int(fd), unix.TIOCGWINSZ) + if err != nil { + return 0, 0, err + } + return int(ws.Col), int(ws.Row), nil +} + +// passwordReader is an io.Reader that reads from a specific file descriptor. +type passwordReader int + +func (r passwordReader) Read(buf []byte) (int, error) { + return unix.Read(int(r), buf) +} + +func readPassword(fd uintptr) ([]byte, error) { + termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios) + if err != nil { + return nil, err + } + + newState := *termios + newState.Lflag &^= unix.ECHO + newState.Lflag |= unix.ICANON | unix.ISIG + newState.Iflag |= unix.ICRNL + if err := unix.IoctlSetTermios(int(fd), ioctlWriteTermios, &newState); err != nil { + return nil, err + } + + defer unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios) + + return readPasswordLine(passwordReader(fd)) +} diff --git a/vendor/github.com/charmbracelet/x/term/term_unix_bsd.go b/vendor/github.com/charmbracelet/x/term/term_unix_bsd.go new file mode 100644 index 0000000..b435031 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/term/term_unix_bsd.go @@ -0,0 +1,11 @@ +//go:build darwin || dragonfly || freebsd || netbsd || openbsd +// +build darwin dragonfly freebsd netbsd openbsd + +package term + +import "golang.org/x/sys/unix" + +const ( + ioctlReadTermios = unix.TIOCGETA + ioctlWriteTermios = unix.TIOCSETA +) diff --git a/vendor/github.com/charmbracelet/x/term/term_unix_other.go b/vendor/github.com/charmbracelet/x/term/term_unix_other.go new file mode 100644 index 0000000..ee2a29e --- /dev/null +++ b/vendor/github.com/charmbracelet/x/term/term_unix_other.go @@ -0,0 +1,11 @@ +//go:build aix || linux || solaris || zos +// +build aix linux solaris zos + +package term + +import "golang.org/x/sys/unix" + +const ( + ioctlReadTermios = unix.TCGETS + ioctlWriteTermios = unix.TCSETS +) diff --git a/vendor/github.com/charmbracelet/x/term/term_windows.go b/vendor/github.com/charmbracelet/x/term/term_windows.go new file mode 100644 index 0000000..fe7afde --- /dev/null +++ b/vendor/github.com/charmbracelet/x/term/term_windows.go @@ -0,0 +1,87 @@ +//go:build windows +// +build windows + +package term + +import ( + "os" + + "golang.org/x/sys/windows" +) + +type state struct { + Mode uint32 +} + +func isTerminal(fd uintptr) bool { + var st uint32 + err := windows.GetConsoleMode(windows.Handle(fd), &st) + return err == nil +} + +func makeRaw(fd uintptr) (*State, error) { + var st uint32 + if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { + return nil, err + } + raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT) + raw |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT + if err := windows.SetConsoleMode(windows.Handle(fd), raw); err != nil { + return nil, err + } + return &State{state{st}}, nil +} + +func setState(fd uintptr, state *State) error { + var mode uint32 + if state != nil { + mode = state.Mode + } + return windows.SetConsoleMode(windows.Handle(fd), mode) +} + +func getState(fd uintptr) (*State, error) { + var st uint32 + if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { + return nil, err + } + return &State{state{st}}, nil +} + +func restore(fd uintptr, state *State) error { + return windows.SetConsoleMode(windows.Handle(fd), state.Mode) +} + +func getSize(fd uintptr) (width, height int, err error) { + var info windows.ConsoleScreenBufferInfo + if err := windows.GetConsoleScreenBufferInfo(windows.Handle(fd), &info); err != nil { + return 0, 0, err + } + return int(info.Window.Right - info.Window.Left + 1), int(info.Window.Bottom - info.Window.Top + 1), nil +} + +func readPassword(fd uintptr) ([]byte, error) { + var st uint32 + if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { + return nil, err + } + old := st + + st &^= (windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT) + st |= (windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_PROCESSED_INPUT) + if err := windows.SetConsoleMode(windows.Handle(fd), st); err != nil { + return nil, err + } + + defer windows.SetConsoleMode(windows.Handle(fd), old) + + var h windows.Handle + p, _ := windows.GetCurrentProcess() + if err := windows.DuplicateHandle(p, windows.Handle(fd), p, &h, 0, false, windows.DUPLICATE_SAME_ACCESS); err != nil { + return nil, err + } + + f := os.NewFile(uintptr(h), "stdin") + defer f.Close() + return readPasswordLine(f) +} diff --git a/vendor/github.com/charmbracelet/x/term/terminal.go b/vendor/github.com/charmbracelet/x/term/terminal.go new file mode 100644 index 0000000..8963163 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/term/terminal.go @@ -0,0 +1,12 @@ +package term + +import ( + "io" +) + +// File represents a file that has a file descriptor and can be read from, +// written to, and closed. +type File interface { + io.ReadWriteCloser + Fd() uintptr +} diff --git a/vendor/github.com/charmbracelet/x/term/util.go b/vendor/github.com/charmbracelet/x/term/util.go new file mode 100644 index 0000000..b731341 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/term/util.go @@ -0,0 +1,47 @@ +package term + +import ( + "io" + "runtime" +) + +// readPasswordLine reads from reader until it finds \n or io.EOF. +// The slice returned does not include the \n. +// readPasswordLine also ignores any \r it finds. +// Windows uses \r as end of line. So, on Windows, readPasswordLine +// reads until it finds \r and ignores any \n it finds during processing. +func readPasswordLine(reader io.Reader) ([]byte, error) { + var buf [1]byte + var ret []byte + + for { + n, err := reader.Read(buf[:]) + if n > 0 { + switch buf[0] { + case '\b': + if len(ret) > 0 { + ret = ret[:len(ret)-1] + } + case '\n': + if runtime.GOOS != "windows" { + return ret, nil + } + // otherwise ignore \n + case '\r': + if runtime.GOOS == "windows" { + return ret, nil + } + // otherwise ignore \r + default: + ret = append(ret, buf[0]) + } + continue + } + if err != nil { + if err == io.EOF && len(ret) > 0 { + return ret, nil + } + return ret, err + } + } +} |
