Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165package tui
import ( "albedo/cli/internal/daemon" "bytes" "context" "encoding/base64" "image" "os" "os/exec" "strings" "sync" "sync/atomic" "time"
"github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/ansi/kitty")
// A chip's thumbnail area, in cells. Cells are taken to be twice as tall as// they are wide.const ( thumbCols = 12 thumbRows = 4 // thumbPixels bounds the longer edge of what is sent to the terminal. thumbPixels = 240)
// thumbnail is an image the terminal holds under id, shown in cols x rows// cells by writing its Unicode placeholders.type thumbnail struct { id, cols, rows int}
// graphics is how kitty images reach this terminal: supported when it shows// them through Unicode placeholders, wrapped when tmux sits in between.type graphics struct { supported, wrapped bool // hint explains why a terminal that could show thumbnails does not. hint string}
var terminalGraphics = sync.OnceValue(func() graphics { return detectGraphics(os.Getenv, func(args ...string) string { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() out, _ := exec.CommandContext(ctx, "tmux", args...).Output() return strings.TrimSpace(string(out)) })})
// detectGraphics reads the terminal from the environment, and through tmux// the terminal of the client attached to it.func detectGraphics(getenv func(string) string, tmux func(args ...string) string) graphics { if getenv("TMUX") == "" { return graphics{supported: placeholderTerminal(getenv("TERM"), getenv("TERM_PROGRAM")) || getenv("KITTY_WINDOW_ID") != ""} } if !placeholderTerminal(tmux("display-message", "-p", "#{client_termname}"), "") { return graphics{} } switch tmux("show-options", "-gv", "allow-passthrough") { case "on", "all": return graphics{supported: true, wrapped: true} } return graphics{hint: "image thumbnails need tmux's allow-passthrough: set -g allow-passthrough on"}}
// placeholderTerminal reports a terminal known to draw kitty images through// Unicode placeholders.func placeholderTerminal(term, program string) bool { return strings.Contains(term, "kitty") || strings.Contains(term, "ghostty") || program == "ghostty"}
// thumbnailIDs numbers transmitted images 16..255: the id travels as a// placeholder's 256-colour foreground, and the renderer may rewrite the first// sixteen as basic colours. A reused id replaces a long-gone image.var thumbnailIDs atomic.Uint32
// transmitThumbnail decodes image, scales it down, and returns the escape// sequence that hands it to the terminal, with where it will be shown. It// returns false for a terminal without placeholders or an image it cannot// decode (WebP).func transmitThumbnail(g graphics, attachment daemon.ImageAttachment) (thumbnail, string, bool) { if !g.supported { return thumbnail{}, "", false } data, err := base64.StdEncoding.DecodeString(attachment.Data) if err != nil { return thumbnail{}, "", false } decoded, _, err := image.Decode(bytes.NewReader(data)) if err != nil { return thumbnail{}, "", false } cols, rows := thumbnailCells(decoded.Bounds().Dx(), decoded.Bounds().Dy()) thumb := thumbnail{id: 16 + int(thumbnailIDs.Add(1)-1)%240, cols: cols, rows: rows} options := &kitty.Options{ Action: kitty.TransmitAndPut, Transmission: kitty.Direct, Format: kitty.PNG, Quiet: 2, ID: thumb.id, VirtualPlacement: true, Columns: cols, Rows: rows, Chunk: true, } if g.wrapped { options.ChunkFormatter = ansi.TmuxPassthrough } var seq strings.Builder if err := kitty.EncodeGraphics(&seq, shrink(decoded, thumbPixels), options); err != nil { return thumbnail{}, "", false } return thumb, seq.String(), true}
// thumbnailCells fits a width x height pixel image into the thumbnail area.func thumbnailCells(width, height int) (int, int) { if width <= 0 || height <= 0 { return thumbCols, thumbRows } // a cell is about twice as tall as wide: rows = cols * height / width / 2 rows := max(1, (thumbCols*height+width)/(2*width)) if rows <= thumbRows { return thumbCols, rows } return max(1, min(thumbCols, (2*thumbRows*width+height/2)/height)), thumbRows}
// shrink scales img so its longer edge is at most edge pixels, sampling the// nearest source pixel.func shrink(img image.Image, edge int) image.Image { bounds := img.Bounds() width, height := bounds.Dx(), bounds.Dy() if width <= edge && height <= edge { return img } scaledW, scaledH := edge, max(1, height*edge/width) if height > width { scaledW, scaledH = max(1, width*edge/height), edge } out := image.NewRGBA(image.Rect(0, 0, scaledW, scaledH)) for y := range scaledH { for x := range scaledW { out.Set(x, y, img.At(bounds.Min.X+x*width/scaledW, bounds.Min.Y+y*height/scaledH)) } } return out}
// placeholderRows are the cells that draw thumb: each one the placeholder// character with its row and column as diacritics. Their foreground names// the image rather than a colour, so it comes from the id, not the theme.func placeholderRows(thumb thumbnail) []string { image := ansi.Style{}.ForegroundColor(ansi.IndexedColor(thumb.id)).String() reset := ansi.Style{}.DefaultForegroundColor().String() rows := make([]string, thumb.rows) for r := range rows { var row strings.Builder row.WriteString(image) for c := range thumb.cols { row.WriteRune(kitty.Placeholder) row.WriteRune(kitty.Diacritic(r)) row.WriteRune(kitty.Diacritic(c)) } row.WriteString(reset) rows[r] = row.String() } return rows}