-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ansi): iterm2: add iTerm2 Inline Image Protocol (#324)
This implements the iTerm2 Inline Image Protocol. The protocol allows applications to display images inline in the terminal. The protocol is documented at https://iterm2.com/documentation-images.html.
- Loading branch information
1 parent
d87966b
commit 1814328
Showing
4 changed files
with
413 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
package iterm2 | ||
|
||
import ( | ||
"strconv" | ||
"strings" | ||
) | ||
|
||
// Auto is a constant that represents the "auto" value. | ||
const Auto = "auto" | ||
|
||
// Cells returns a string that represents the number of cells. This is simply a | ||
// wrapper around strconv.Itoa. | ||
func Cells(n int) string { | ||
return strconv.Itoa(n) | ||
} | ||
|
||
// Pixels returns a string that represents the number of pixels. | ||
func Pixels(n int) string { | ||
return strconv.Itoa(n) + "px" | ||
} | ||
|
||
// Percent returns a string that represents the percentage. | ||
func Percent(n int) string { | ||
return strconv.Itoa(n) + "%" | ||
} | ||
|
||
// file represents the optional arguments for the iTerm2 Inline Image Protocol. | ||
// | ||
// See https://iterm2.com/documentation-images.html | ||
type file struct { | ||
// Name is the name of the file. Defaults to "Unnamed file" if empty. | ||
Name string | ||
// Size is the file size in bytes. Used for progress indication. This is | ||
// optional. | ||
Size int64 | ||
// Width is the width of the image. This can be specified by a number | ||
// followed by by a unit or "auto". The unit can be none, "px" or "%". None | ||
// means the number is in cells. Defaults to "auto" if empty. | ||
// For convenience, the [Pixels], [Cells] and [Percent] functions and | ||
// [Auto] can be used. | ||
Width string | ||
// Height is the height of the image. This can be specified by a number | ||
// followed by by a unit or "auto". The unit can be none, "px" or "%". None | ||
// means the number is in cells. Defaults to "auto" if empty. | ||
// For convenience, the [Pixels], [Cells] and [Percent] functions and | ||
// [Auto] can be used. | ||
Height string | ||
// IgnoreAspectRatio is a flag that indicates that the image should be | ||
// stretched to fit the specified width and height. Defaults to false | ||
// meaning the aspect ratio is preserved. | ||
IgnoreAspectRatio bool | ||
// Inline is a flag that indicates that the image should be displayed | ||
// inline. Otherwise, it is downloaded to the Downloads folder with no | ||
// visual representation in the terminal. Defaults to false. | ||
Inline bool | ||
// DoNotMoveCursor is a flag that indicates that the cursor should not be | ||
// moved after displaying the image. This is an extension introduced by | ||
// WezTerm and might not work on all terminals supporting the iTerm2 | ||
// protocol. Defaults to false. | ||
DoNotMoveCursor bool | ||
// Content is the base64 encoded data of the file. | ||
Content []byte | ||
} | ||
|
||
// String implements fmt.Stringer. | ||
func (f file) String() string { | ||
var opts []string | ||
if f.Name != "" { | ||
opts = append(opts, "name="+f.Name) | ||
} | ||
if f.Size != 0 { | ||
opts = append(opts, "size="+strconv.FormatInt(f.Size, 10)) | ||
} | ||
if f.Width != "" { | ||
opts = append(opts, "width="+f.Width) | ||
} | ||
if f.Height != "" { | ||
opts = append(opts, "height="+f.Height) | ||
} | ||
if f.IgnoreAspectRatio { | ||
opts = append(opts, "preserveAspectRatio=0") | ||
} | ||
if f.Inline { | ||
opts = append(opts, "inline=1") | ||
} | ||
if f.DoNotMoveCursor { | ||
opts = append(opts, "doNotMoveCursor=1") | ||
} | ||
return strings.Join(opts, ";") | ||
} | ||
|
||
// File represents the optional arguments for the iTerm2 Inline Image Protocol. | ||
type File file | ||
|
||
// String implements fmt.Stringer. | ||
func (f File) String() string { | ||
var s strings.Builder | ||
s.WriteString("File=") | ||
s.WriteString(file(f).String()) | ||
if len(f.Content) > 0 { | ||
s.WriteString(":") | ||
s.Write(f.Content) | ||
} | ||
|
||
return s.String() | ||
} | ||
|
||
// MultipartFile represents the optional arguments for the iTerm2 Inline Image Protocol. | ||
type MultipartFile file | ||
|
||
// String implements fmt.Stringer. | ||
func (f MultipartFile) String() string { | ||
return "MultipartFile=" + file(f).String() | ||
} | ||
|
||
// FilePart represents the optional arguments for the iTerm2 Inline Image Protocol. | ||
type FilePart file | ||
|
||
// String implements fmt.Stringer. | ||
func (f FilePart) String() string { | ||
return "FilePart=" + string(f.Content) | ||
} | ||
|
||
// FileEnd represents the optional arguments for the iTerm2 Inline Image Protocol. | ||
type FileEnd struct{} | ||
|
||
// String implements fmt.Stringer. | ||
func (f FileEnd) String() string { | ||
return "FileEnd" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
package iterm2 | ||
|
||
import ( | ||
"encoding/base64" | ||
"testing" | ||
) | ||
|
||
func TestCells(t *testing.T) { | ||
tests := []struct { | ||
input int | ||
want string | ||
}{ | ||
{0, "0"}, | ||
{10, "10"}, | ||
{-5, "-5"}, | ||
{100, "100"}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.want, func(t *testing.T) { | ||
if got := Cells(tt.input); got != tt.want { | ||
t.Errorf("Cells(%d) = %v, want %v", tt.input, got, tt.want) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestPixels(t *testing.T) { | ||
tests := []struct { | ||
input int | ||
want string | ||
}{ | ||
{0, "0px"}, | ||
{10, "10px"}, | ||
{-5, "-5px"}, | ||
{100, "100px"}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.want, func(t *testing.T) { | ||
if got := Pixels(tt.input); got != tt.want { | ||
t.Errorf("Pixels(%d) = %v, want %v", tt.input, got, tt.want) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestPercent(t *testing.T) { | ||
tests := []struct { | ||
input int | ||
want string | ||
}{ | ||
{0, "0%"}, | ||
{10, "10%"}, | ||
{-5, "-5%"}, | ||
{100, "100%"}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.want, func(t *testing.T) { | ||
if got := Percent(tt.input); got != tt.want { | ||
t.Errorf("Percent(%d) = %v, want %v", tt.input, got, tt.want) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestFile_String(t *testing.T) { | ||
sampleContent := []byte("test-content") | ||
tests := []struct { | ||
name string | ||
file file | ||
want string | ||
}{ | ||
{ | ||
name: "empty file", | ||
file: file{}, | ||
want: "", | ||
}, | ||
{ | ||
name: "basic file", | ||
file: file{ | ||
Name: "test.png", | ||
Size: 1024, | ||
}, | ||
want: "name=test.png;size=1024", | ||
}, | ||
{ | ||
name: "file with dimensions", | ||
file: file{ | ||
Name: "test.png", | ||
Width: "100px", | ||
Height: "auto", | ||
}, | ||
want: "name=test.png;width=100px;height=auto", | ||
}, | ||
{ | ||
name: "file with all options", | ||
file: file{ | ||
Name: "test.png", | ||
Size: 1024, | ||
Width: "100px", | ||
Height: "50%", | ||
IgnoreAspectRatio: true, | ||
Inline: true, | ||
DoNotMoveCursor: true, | ||
Content: sampleContent, | ||
}, | ||
want: "name=test.png;size=1024;width=100px;height=50%;preserveAspectRatio=0;inline=1;doNotMoveCursor=1", | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
if got := tt.file.String(); got != tt.want { | ||
t.Errorf("file.String() = %v, want %v", got, tt.want) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestFile_String_WithContent(t *testing.T) { | ||
sampleContent := []byte("test-content") | ||
encodedContent := base64.StdEncoding.EncodeToString(sampleContent) | ||
|
||
f := File{ | ||
Name: "test.png", | ||
Content: []byte(encodedContent), | ||
} | ||
|
||
want := "File=name=test.png:" + encodedContent | ||
if got := f.String(); got != want { | ||
t.Errorf("File.String() = %v, want %v", got, want) | ||
} | ||
} | ||
|
||
func TestMultipartFile_String(t *testing.T) { | ||
f := MultipartFile{ | ||
Name: "test.png", | ||
Size: 1024, | ||
Width: "100px", | ||
Height: "50%", | ||
} | ||
|
||
want := "MultipartFile=name=test.png;size=1024;width=100px;height=50%" | ||
if got := f.String(); got != want { | ||
t.Errorf("MultipartFile.String() = %v, want %v", got, want) | ||
} | ||
} | ||
|
||
func TestFilePart_String(t *testing.T) { | ||
sampleContent := []byte("test-content") | ||
f := FilePart{ | ||
Content: sampleContent, | ||
} | ||
|
||
want := "FilePart=" + string(sampleContent) | ||
if got := f.String(); got != want { | ||
t.Errorf("FilePart.String() = %v, want %v", got, want) | ||
} | ||
} | ||
|
||
func TestFileEnd_String(t *testing.T) { | ||
f := FileEnd{} | ||
want := "FileEnd" | ||
if got := f.String(); got != want { | ||
t.Errorf("FileEnd.String() = %v, want %v", got, want) | ||
} | ||
} | ||
|
||
func TestAuto_Constant(t *testing.T) { | ||
if Auto != "auto" { | ||
t.Errorf("Auto constant = %v, want 'auto'", Auto) | ||
} | ||
} |
Oops, something went wrong.