package tui import ( "bytes" "encoding/base64" "fmt" "image" "image/color" _ "image/jpeg" _ "image/png" "io" "net/http" "os" "strings" "time" "github.com/disintegration/imaging" "github.com/mattn/go-sixel" ) func FetchAndProcessThumbnail(url string) (string, error) { if url == "" { return "", fmt.Errorf("empty URL") } client := &http.Client{ Timeout: 10 * time.Second, } resp, err := client.Get(url) if err != nil { return "", fmt.Errorf("failed to fetch image: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("failed to fetch image: status %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("failed to read image data: %w", err) } img, _, err := image.Decode(bytes.NewReader(body)) if err != nil { return "", fmt.Errorf("failed to decode image: %w", err) } // Consistent thumbnail size width := 40 height := 15 img = imaging.Resize(img, width*2, height*2, imaging.Lanczos) // Try iTerm2 protocol first if isITerm() { return getITermImageSequence(img), nil } // Try Sixel next var buf bytes.Buffer enc := sixel.NewEncoder(&buf) err = enc.Encode(img) if err == nil { return buf.String(), nil } // Fall back to ASCII art return imageToASCII(img), nil } func imageToASCII(img image.Image) string { bounds := img.Bounds() width := 40 height := 15 chars := []string{"█", "▓", "▒", "░", " "} var ascii strings.Builder stepX := float64(bounds.Dx()) / float64(width) stepY := float64(bounds.Dy()) / float64(height) for y := 0; y < height; y++ { for x := 0; x < width; x++ { px := int(float64(x) * stepX) py := int(float64(y) * stepY) c := color.GrayModel.Convert(img.At(px+bounds.Min.X, py+bounds.Min.Y)).(color.Gray) index := int(c.Y) * (len(chars) - 1) / 255 ascii.WriteString(chars[index]) } ascii.WriteString("\n") } return ascii.String() } func getITermImageSequence(img image.Image) string { var buf bytes.Buffer imaging.Encode(&buf, img, imaging.PNG) encoded := base64.StdEncoding.EncodeToString(buf.Bytes()) return fmt.Sprintf("\033]1337;File=inline=1;width=40ch;height=15;preserveAspectRatio=1:%s\a", encoded) } func isITerm() bool { return os.Getenv("TERM_PROGRAM") == "iTerm.app" }