forked from toolshed/abra
We were running behind and there were quite some deprecations to update. This was mostly in the upstream copy/pasta package but seems quite minimal.
200 lines
5.2 KiB
Go
200 lines
5.2 KiB
Go
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
|
|
}
|