Skip to content

Painting a region yourself

termopy owns the terminal: the alternate screen, raw mode, and a painter that diffs rows between frames. Anything it does not model has no way onto the screen, and an app that writes its own escape codes will be overwritten or leave debris.

View.passthrough is the sanctioned way through:

View.passthrough(width, height, emit)

It reserves width × height blank cells (so layout is unaffected and whatever is behind shows through) and calls emit with the region's settled position each frame. Whatever emit returns is written after the cells beneath it.

def image(renderer, path: Path, width: int, height: int) -> View:
    def emit(region: Region) -> str:
        return renderer.render(path, region.width, region.height)

    return View.passthrough(width, height, emit)

The region travels with the view through hcat, vcat, pad and crop, so emit receives the position layout settled on.

Why the painter has to know

Row diffing cannot see these regions. The cells beneath are blank and compare equal frame to frame, so left alone nothing would ever repaint an image, and nothing would clear one that had gone away.

The painter remembers what each region last emitted and repaints when either the output changes or the rows beneath it were redrawn. An unchanged image is not resent, which matters when the kitty protocol payload for one photo is 3.6 MB.

Uses

Images are the first caller. The same hole covers OSC 8 hyperlinks, iTerm2 marks, terminal notifications, and whatever protocol appears next. termopy deliberately has no image support: it has a region you can paint, and image protocols are an application's business.

Prezo's 1,602 lines of kitty, sixel, iTerm2 and chafa renderers ported unchanged, because they already produced an escape string for a given cell rectangle, which is exactly what emit returns.

Full-screen handover

For something that wants the whole terminal (an image viewer, an editor, a pager), use ui.suspend().