mirror of
https://github.com/zoriya/bubbles.git
synced 2026-08-05 04:36:07 +00:00
wip: iterm2
This commit is contained in:
@@ -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
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user