package main import ( "bytes" "encoding/base64" "fmt" "image" "image/color" "image/png" "log" "math/rand" "os" "path/filepath" "strings" uv "github.com/charmbracelet/ultraviolet" "github.com/charmbracelet/ultraviolet/screen" "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/ansi/iterm2" ) func main() { t := uv.DefaultTerminal() if err := t.Start(); err != nil { log.Fatalln("error starting terminal:", err) } defer t.Stop() scr := t.Screen() scr.EnterAltScreen() var keypress string var cellWidth, cellHeight int tmpDir, _ := os.MkdirTemp("", "uv-rgbimage-example") defer os.RemoveAll(tmpDir) createEncodedImage := func(w, h int, col color.Color) string { img := createImage(w*cellWidth, h*cellHeight, col) png := imagePng(img) if err := os.WriteFile(filepath.Join(tmpDir, fmt.Sprintf("img_%d_%d.png", w, h)), png, 0o644); err != nil { log.Println("error writing png file:", err) } return dataBase64(png) } var imgXOffset, imgYOffset int display := func() { screen.Clear(scr) text := fmt.Sprintf("Press any key to see its value. Press 'ctrl+c' to quit.\n\nLast key pressed: %s", keypress) nlines := strings.Count(text, "\n") + 1 ss := uv.NewStyledString(text) bounds := scr.Bounds() if bounds.Empty() { return } center := uv.Rect( (bounds.Max.X-ss.UnicodeWidth())/2, (bounds.Max.Y-nlines)/2, ss.UnicodeWidth(), nlines, ) ss.Draw(scr, center) scr.Render() const imgW, imgH = 10, 5 col := ansi.IndexedColor(rand.Intn(256)) encodedPngImg := createEncodedImage(imgW, imgH, col) centerImg := uv.Rect( 0+imgXOffset, 0+imgYOffset, // ((bounds.Max.X-imgW)/2)+imgXOffset, // ((bounds.Max.Y-imgH)/2)+imgYOffset, imgW, imgH, ) log.Printf("center image rect: %+v", centerImg) for y := centerImg.Min.Y; y < centerImg.Max.Y; y++ { log.Printf("setting cell at (%d, %d)", centerImg.Min.X, y) var content string if y == centerImg.Min.Y { content = ansi.ITerm2(iterm2.File{ Name: "img.png", Width: iterm2.Cells(imgW), Height: iterm2.Cells(imgH), Content: []byte(encodedPngImg), Inline: true, IgnoreAspectRatio: true, DoNotMoveCursor: false, }) + // ansi.CursorUp(imgH-1) + // ansi.CursorForward(imgW+1) ansi.CursorPosition(centerImg.Min.X+imgW+1, centerImg.Min.Y+1) log.Printf("content for cell at (%d, %d): %q", centerImg.Min.X, y, content) } else { content = ansi.CursorForward(imgW) } scr.SetCell(centerImg.Min.X, y, &uv.Cell{ Content: content, Width: imgW, }) } scr.Render() scr.Flush() } display() loop: for ev := range t.Events() { switch e := ev.(type) { case uv.PixelSizeEvent: cellWidth = e.Width / scr.Bounds().Dx() cellHeight = e.Height / scr.Bounds().Dy() case uv.WindowSizeEvent: scr.Resize(e.Width, e.Height) case uv.KeyPressEvent: switch e.String() { case "ctrl+c": break loop case "up": imgYOffset-- case "down": imgYOffset++ case "left": imgXOffset-- case "right": imgXOffset++ } keypress = e.String() } display() } screen.Clear(scr) scr.Render() scr.Reset() scr.Flush() } func createImage(w, h int, col color.Color) *image.RGBA { img := image.NewRGBA(image.Rect(0, 0, w, h)) for y := range h { for x := range w { img.Set(x, y, col) } } return img } func imageRgba(img image.Image) []byte { var buf bytes.Buffer bounds := img.Bounds() for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for x := bounds.Min.X; x < bounds.Max.X; x++ { r, g, b, a := img.At(x, y).RGBA() buf.WriteByte(byte(r >> 8)) buf.WriteByte(byte(g >> 8)) buf.WriteByte(byte(b >> 8)) buf.WriteByte(byte(a >> 8)) } } return buf.Bytes() } func imagePng(img image.Image) []byte { var buf bytes.Buffer png.Encode(&buf, img) return buf.Bytes() } func dataBase64(data []byte) string { return base64.StdEncoding.EncodeToString(data) } func init() { f, err := os.OpenFile("uv_debug.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) if err != nil { log.Fatalln("error opening debug log file:", err) } log.SetOutput(f) log.SetFlags(log.LstdFlags | log.Lshortfile) }