Skip to content

Commit

Permalink
feat(ansi): kitty: support encoding.TextMarshaler and encoding.TextUn…
Browse files Browse the repository at this point in the history
…marshaler
  • Loading branch information
aymanbagabas committed Jan 13, 2025
1 parent 7c624d3 commit 92b53c8
Show file tree
Hide file tree
Showing 2 changed files with 270 additions and 2 deletions.
103 changes: 101 additions & 2 deletions ansi/kitty/options.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
package kitty

import (
"encoding"
"fmt"
"strconv"
"strings"
)

var (
_ encoding.TextMarshaler = Options{}
_ encoding.TextUnmarshaler = &Options{}
)

// Options represents a Kitty Graphics Protocol options.
Expand Down Expand Up @@ -45,7 +53,7 @@ type Options struct {
Compression byte

// Transmission (t=d) is the image transmission type. Can be [Direct], [File],
// [TempFile], or [SharedMemory].
// [TempFile], or[SharedMemory].
Transmission byte

// File is the file path to be used when the transmission type is [File].
Expand Down Expand Up @@ -101,7 +109,7 @@ type Options struct {
// scaled to fit the number of rows.
Rows int

// VirtualPlacement (V=0) whether to use virtual placement. This is used
// VirtualPlacement (U=0) whether to use virtual placement. This is used
// with Unicode [Placeholder] to display images.
VirtualPlacement bool

Expand Down Expand Up @@ -258,3 +266,94 @@ func (o *Options) Options() (opts []string) {

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 "q":
o.Quite = 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", "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 "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
}
169 changes: 169 additions & 0 deletions ansi/kitty/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,175 @@ func TestOptions_Validation(t *testing.T) {
}
}

func TestOptions_String(t *testing.T) {
tests := []struct {
name string
o Options
want string
}{
{
name: "empty options",
o: Options{},
want: "",
},
{
name: "full options",
o: Options{
Action: 'A',
Quite: 'Q',
Compression: 'C',
Transmission: 'T',
Delete: 'd',
DeleteResources: true,
ID: 123,
PlacementID: 456,
Number: 789,
Format: 1,
ImageWidth: 800,
ImageHeight: 600,
Size: 1024,
Offset: 10,
Chunk: true,
X: 100,
Y: 200,
Z: 300,
Width: 400,
Height: 500,
OffsetX: 50,
OffsetY: 60,
Columns: 4,
Rows: 3,
VirtualPlacement: true,
ParentID: 999,
ParentPlacementID: 888,
},
want: "f=1,q=81,i=123,p=456,I=789,s=800,v=600,t=T,S=1024,O=10,U=1,P=999,Q=888,x=100,y=200,z=300,w=400,h=500,X=50,Y=60,c=4,r=3,d=D,a=A",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.o.String(); got != tt.want {
t.Errorf("Options.String() = %v, want %v", got, tt.want)
}
})
}
}

func TestOptions_MarshalText(t *testing.T) {
tests := []struct {
name string
o Options
want []byte
wantErr bool
}{
{
name: "marshal empty options",
o: Options{},
want: []byte(""),
},
{
name: "marshal with values",
o: Options{
Action: 'A',
ID: 123,
Width: 400,
Height: 500,
},
want: []byte("i=123,w=400,h=500,a=A"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.o.MarshalText()
if (err != nil) != tt.wantErr {
t.Errorf("Options.MarshalText() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Options.MarshalText() = %q, want %q", got, tt.want)
}
})
}
}

func TestOptions_UnmarshalText(t *testing.T) {
tests := []struct {
name string
text []byte
want Options
wantErr bool
}{
{
name: "unmarshal empty",
text: []byte(""),
want: Options{},
},
{
name: "unmarshal basic options",
text: []byte("a=A,i=123,w=400,h=500"),
want: Options{
Action: 'A',
ID: 123,
Width: 400,
Height: 500,
},
},
{
name: "unmarshal with invalid number",
text: []byte("i=abc"),
want: Options{},
},
{
name: "unmarshal with delete resources",
text: []byte("d=D"),
want: Options{
Delete: 'd',
DeleteResources: true,
},
},
{
name: "unmarshal with boolean chunk",
text: []byte("m=1"),
want: Options{
Chunk: true,
},
},
{
name: "unmarshal with virtual placement",
text: []byte("U=1"),
want: Options{
VirtualPlacement: true,
},
},
{
name: "unmarshal with invalid format",
text: []byte("invalid=format"),
want: Options{},
},
{
name: "unmarshal with missing value",
text: []byte("a="),
want: Options{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var o Options
err := o.UnmarshalText(tt.text)
if (err != nil) != tt.wantErr {
t.Errorf("Options.UnmarshalText() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(o, tt.want) {
t.Errorf("Options.UnmarshalText() = %+v, want %+v", o, tt.want)
}
})
}
}

// Helper functions

func sortStrings(s []string) {
Expand Down

0 comments on commit 92b53c8

Please sign in to comment.