| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- 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
- }
|