graphics

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 7, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

Documentation

Overview

Package graphics puts images in a terminal that can show them.

It writes escape sequences and nothing else: it does not decode an image, hold one, decide where it goes, or know what a cell contains. What it needs is a PNG that somebody else already has and a place the caller has already worked out.

Why only PNG

PNG because its dimensions are in the first twenty-four bytes, which means the size can be known without a decoder and therefore without a dependency. The promise this library makes about its dependency list is worth more than the convenience of accepting a JPEG.

The protocols, and what each is good for

Showing an image and showing one in an interface that redraws are two different capabilities, and only one protocol has both.

Kitty's gives the program a handle: an image is sent once under a number, placed as often as needed, moved, and deleted. That is what a live region requires — what it showed last frame has to be moved or taken away this frame, and a protocol with no way to name an image has no way to be told which one.

iTerm2's protocol and sixel put pixels at the cursor and end there. No number, no z-order, no deletion. They are usable where nothing will redraw over the result — printed output, which belongs to the terminal from then on — and not in a region being drawn again sixty times a second. Protocol.Supports is that distinction, and it is why this package names more protocols than it writes.

What it writes is kitty and iTerm2. Sixel is detected and reported and not produced: producing it means decoding the image into pixels, and a decoder is the dependency this package exists without. A caller holding an encoder of its own learns from Sixel that the terminal will take what it makes.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotPNG = errors.New("graphics: not a PNG")

ErrNotPNG is reported for data that is not a PNG this package can size.

Functions

func Delete

func Delete(w io.Writer, id uint32) error

Delete removes every placement of an image and forgets it.

func Fit

func Fit(pxW, pxH, cellW, cellH, maxCols, maxRows int) (cols, rows int)

Fit is the cell box an image of pxW by pxH should occupy, keeping its aspect ratio and staying inside maxCols by maxRows.

cellW and cellH are the size of one cell in pixels, which a terminal reports and a caller has to have asked for. Nothing sensible can be computed without them, so an unusable argument gives a single cell rather than a division by zero.

func Inline added in v0.0.2

func Inline(w io.Writer, png []byte, cols, rows int) error

Inline writes an image at the cursor over iTerm2's protocol.

There is no counterpart to Place or Delete, because the protocol has none: the image goes where the cursor is and the program never hears of it again. That is why it is only for Printed output — see the package comment — and why this takes the payload every time rather than an identifier.

The box is in cells, and the image is fitted inside it rather than stretched to it, so a caller passes what Fit worked out and gets the aspect ratio kept.

func PNGSize

func PNGSize(png []byte) (width, height int, err error)

PNGSize is the pixel size of a PNG, read from its header.

It reads the signature and the IHDR chunk and nothing else. A decoder would be a dependency, and every byte it would decode is a byte this package has no use for: the pixels are the terminal's problem, and only the dimensions are ours.

func Place

func Place(w io.Writer, id uint32, cols, rows int) error

Place shows a transmitted image at the cursor, scaled into cols by rows cells.

The cursor is positioned by the caller, and the escape belongs after the cell diff of the frame it appears in: the diff would otherwise write over the image with the blanks it thinks are underneath it.

The cursor is left where it was found. That is what lets an image go in a frame at all — every position in a frame is a movement from the last known one, and an inline block's whole position is relative — and it is the property the protocols that cannot be told to move an image also lack. See Image.Paint, which is this under the name a frame asks for.

Types

type Image

type Image struct {
	// ID is the number the terminal now knows the image by, which is what
	// [Place] and [Delete] refer to.
	ID uint32
	// Width and Height are the image's size in pixels, for working out how many
	// cells it should occupy with [Fit].
	Width, Height int
}

Image is a transmitted image and the size it arrived at.

func Transmit

func Transmit(w io.Writer, id uint32, png []byte) (Image, error)

Transmit sends a PNG to the terminal under an ID, without placing it.

Transmission and placement are separate because they happen at different times: an image is sent once and placed on every frame that shows it, and re-sending the payload each frame would put a megabyte on the wire to move a picture by one row.

func (Image) Erase added in v0.0.3

func (i Image) Erase(w io.Writer) error

Erase removes every placement of the image and forgets it.

func (Image) Paint added in v0.0.3

func (i Image) Paint(w io.Writer, cols, rows int) error

Paint puts the image in a region of a frame, and Erase takes it away again.

The two of them are what a frame asks of anything that writes itself onto the terminal rather than into cells — see github.com/Tangerg/oolong/core/grid.Painter, which this satisfies without either package knowing about the other: one says what a region needs, the other happens to be able to do it.

Only an image that was transmitted has them, which is the same distinction the package comment draws. An image put on the screen with Inline has no name to place again or take away, so there is nothing here for it to be.

type Placement added in v0.0.2

type Placement uint8

Placement is where an image is going, which is what decides whether a protocol will do.

const (
	// Printed is output written once that then belongs to the terminal, scrolling
	// away with the rest of the session. Nothing draws over it again, so a protocol
	// that cannot be told to remove an image is no worse off here than one that can.
	//
	// It is the zero value because it is the weaker requirement: code that has not
	// said where an image is going gets the answer that holds in both places.
	Printed Placement = iota
	// Live is a region the interface redraws. An image there has to be placeable
	// again on the next frame and removable on the frame after, which takes a
	// protocol that lets an image be named.
	Live
)

type Protocol

type Protocol uint8

Protocol is the inline-image capability of a terminal.

The zero value is None, so anything that has not been told what it is talking to draws no images rather than corrupting a screen with escape sequences the terminal will print instead of obey.

const (
	// None is a terminal that cannot show an image. A caller draws a placeholder.
	None Protocol = iota
	// Kitty is the kitty graphics protocol: kitty, Ghostty, WezTerm and Warp. It is
	// the only one usable in a region the interface redraws.
	Kitty
	// ITerm2 is iTerm2's own inline-image protocol, also spoken by WezTerm and
	// mintty. An image goes at the cursor and cannot be referred to again.
	ITerm2
	// Sixel is the oldest of the three and the most widely implemented after
	// kitty's: xterm, foot, mlterm, contour, and recent Windows Terminal. This
	// package reports it and does not write it — see the package comment.
	Sixel
)

func DetectIn

func DetectIn(getenv func(string) string, name string, sixel bool) Protocol

DetectIn works out the richest protocol a terminal supports.

name is what the terminal said it was when asked, or empty when nothing was asked or nothing answered. It outranks the environment for the reason above.

It takes these facts because fewer are not enough. The environment names the terminal, which is how kitty's protocol and iTerm2's are found; nothing in the environment names sixel, so a terminal that supports sixel and nothing else is indistinguishable from a terminal that supports nothing. That answer only comes from a device-attribute response, and sixel says what it said.

The lookup is passed in rather than read, which is what makes this a function of its inputs: an adapter passes its environment lookup and negotiated answers, and a test passes whatever facts it wants. There is no cached global and no override hook, for the same reason there is no global palette — a program with two terminals could not have two answers, and a test could not pin either.

func (Protocol) String added in v0.0.2

func (p Protocol) String() string

String names the protocol the way a diagnostic would.

func (Protocol) Supports added in v0.0.2

func (p Protocol) Supports(where Placement) bool

Supports reports whether an image can go in that place over this protocol.

Asking it is worth more than a single yes or no about the terminal. A terminal that draws images but cannot be told to move them is a different thing to tell the user about from one that draws none: the first shows a picture in printed output and nothing in a live view, and a caller holding one boolean can explain neither.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL