wip: iterm2

This commit is contained in:
Ayman Bagabas
2025-02-06 15:55:59 -05:00
parent 244c46d671
commit b6f90bbf6f
2 changed files with 60 additions and 32 deletions
+2
View File
@@ -4,6 +4,8 @@ go 1.22
replace github.com/charmbracelet/x/cellbuf => ../x/cellbuf replace github.com/charmbracelet/x/cellbuf => ../x/cellbuf
replace github.com/charmbracelet/bubbletea/v2 => ../bubbletea
require ( require (
github.com/MakeNowJust/heredoc v1.0.0 github.com/MakeNowJust/heredoc v1.0.0
github.com/atotto/clipboard v0.1.4 github.com/atotto/clipboard v0.1.4
+58 -32
View File
@@ -2,18 +2,18 @@ package image
import ( import (
"bytes" "bytes"
"encoding/base64"
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
"log" "image/png"
"os" "os"
"path/filepath"
"strings" "strings"
"sync/atomic" "sync/atomic"
"time"
tea "github.com/charmbracelet/bubbletea/v2" tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/ansi"
"github.com/charmbracelet/x/ansi/iterm2"
"github.com/charmbracelet/x/ansi/kitty" "github.com/charmbracelet/x/ansi/kitty"
"github.com/charmbracelet/x/input" "github.com/charmbracelet/x/input"
) )
@@ -51,6 +51,9 @@ type Model struct {
// The image options // The image options
opts kitty.Options opts kitty.Options
// seq contains the encoded image sequence buffer to render the image.
seq string
// The image unique id. A non-zero indicates the image was transmitted successfully. // The image unique id. A non-zero indicates the image was transmitted successfully.
id int id int
// The image number // The image number
@@ -58,9 +61,14 @@ type Model struct {
// The terminal width and height // The terminal width and height
w, h int w, h int
// laterDraw indicates if the image is being drawn for the first time.
laterDraw bool
} }
func newModel(area image.Rectangle) (m Model) { func newModel(area image.Rectangle) (m Model) {
m.Protocol = ITerm2
// We always use virtual placement for images // We always use virtual placement for images
m.opts.VirtualPlacement = true m.opts.VirtualPlacement = true
// Always chunk the image // Always chunk the image
@@ -83,15 +91,6 @@ func NewLocal(file string, area image.Rectangle) (m Model, err error) {
m.file = file m.file = file
m.area = area m.area = area
// Quick check for PNG files
ext := filepath.Ext(file)
if strings.Contains(ext, "png") {
// We're done here, there's no need to decode the image.
m.opts.Format = kitty.PNG
m.opts.File = file
return
}
f, err := os.Open(file) f, err := os.Open(file)
if err != nil { if err != nil {
return m, fmt.Errorf("could not open image file: %w", err) return m, fmt.Errorf("could not open image file: %w", err)
@@ -171,30 +170,48 @@ func (m Model) Area() image.Rectangle {
return m.area return m.area
} }
// transmit is a command that transmits the image to the terminal. // imageMsg is a message that transmits the image to the terminal.
func (m *Model) transmit() tea.Msg { type imageMsg string
// IDK why, but we need to wait a bit before transmitting the image
// to the terminal. Otherwise, the image is not displayed.
time.Sleep(100 * time.Millisecond)
// renderKitty returns the Kitty graphics sequence to render the image.
func (m *Model) renderKittyCmd() tea.Msg {
var seq bytes.Buffer var seq bytes.Buffer
if err := ansi.WriteKittyGraphics(&seq, m.m, &m.opts); err != nil { if err := ansi.WriteKittyGraphics(&seq, m.m, &m.opts); err != nil {
log.Printf("could not transmit image: %v", err)
// TODO: Error handling // TODO: Error handling
return nil return imageMsg("")
} }
return tea.RawMsg{Msg: seq.String()} return imageMsg(seq.String())
}
// renderIterm2 returns the iTerm2 graphics sequence to render the image.
func (m *Model) renderIterm2Cmd() tea.Msg {
var buf bytes.Buffer
enc := base64.NewEncoder(base64.StdEncoding, &buf)
if err := png.Encode(enc, m.m); err != nil {
return imageMsg("")
}
if err := enc.Close(); err != nil {
return imageMsg("")
}
return imageMsg(ansi.ITerm2(iterm2.File{
Width: iterm2.Cells(m.area.Dx()),
Height: iterm2.Cells(m.area.Dy()),
Inline: true,
// DoNotMoveCursor: true,
IgnoreAspectRatio: true,
Content: buf.Bytes(),
}))
} }
// Init initializes the image model. // Init initializes the image model.
func (m Model) Init() (tea.Model, tea.Cmd) { func (m Model) Init() tea.Cmd {
return m, tea.Batch( return tea.Batch(
// TODO: Query support // m.renderKittyCmd,
func() tea.Msg { m.renderIterm2Cmd,
time.Sleep(1 * time.Second) // TODO: Query support
return m.transmit()
},
) )
} }
@@ -204,6 +221,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
m.w, m.h = msg.Width, msg.Height m.w, m.h = msg.Width, msg.Height
cmds = append(cmds, tea.ClearScreen)
case input.KittyGraphicsEvent: case input.KittyGraphicsEvent:
if msg.Options.Number == m.num && if msg.Options.Number == m.num &&
msg.Options.ID > 0 && msg.Options.ID > 0 &&
@@ -211,18 +229,17 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Store the actual image id // Store the actual image id
m.id = msg.Options.ID m.id = msg.Options.ID
} }
case imageMsg:
m.seq = string(msg)
} }
m.laterDraw = true
return m, tea.Batch(cmds...) return m, tea.Batch(cmds...)
} }
// View returns a string representation to render the image. // View returns a string representation to render the image.
func (m Model) View() string { func (m Model) View() string {
if m.id == 0 {
// TODO: Maybe use a spinner?
return "Loading image..."
}
// Build Kitty graphics unicode place holders // Build Kitty graphics unicode place holders
var fgSeq string var fgSeq string
var extra int var extra int
@@ -267,6 +284,15 @@ func (m Model) View() string {
} }
} }
if m.laterDraw && m.Protocol == ITerm2 {
// Move the cursor to the top left corner of the image
s.WriteString(ansi.CursorBackward(m.area.Dx()))
s.WriteString(ansi.CursorUp(m.area.Dy()))
}
// Write the image sequence
s.WriteString(m.seq)
return s.String() return s.String()
} }