Ubunatic Coding
terminal image viewer

cat for
images.

cati is the focused CLI for rendering image files and directories in your terminal. The media player and browser now live in separate binaries, so the core renderer and public Go library can stay small and reliable.

Recorded demo: static SVG rendering, spark/best mode, image sequences, and the player forwarder.
capabilities

Everything you need to view images in the terminal

Core rendering first. Optional media and browser tools when you need them.

▀▄

Half-block rendering

Each terminal cell encodes two vertical pixels using ▀ ▄ █ and 24-bit true-color ANSI, doubling effective vertical resolution.

📹

Separate media player

catiplay handles videos and image sequences. cati play remains as a compatibility forwarder.

🔍

SVG and raster inputs

Render PNG, JPEG, GIF, WebP, and SVG. SVGs are rasterized at the target display size with browser-like absolute CSS units.

🖼️

Separate file browser

catibrowse previews directories and opens selected assets through catiplay. cati browse forwards to it.

Auto-scaling

Detects terminal size via TIOCGWINSZ and scales media down to fit perfectly — content never breaks your layout.

Public Go library

Use the renderer directly from Go through the versioned v1 packages for terminal UIs and tooling.

current status

One library, three focused commands

The split keeps core rendering work from being slowed down by optional TUI features.

catiprimary

Simple CLI for rendering single files, multiple files, and directories. This is the main target for algorithm tuning, resilience, integration tests, and preflight checks.

catiplayoptional

Terminal media player for videos and image sequences. It is built and installed, but its heavier tests are guarded behind an explicit Go build tag.

catibrowsedemo

Directory browser with previews. It demonstrates what can be built on top of the public library and launches catiplay for selected assets.

technique

Two pixels, one cell.

Unicode block characters divide a terminal cell into a top half and a bottom half. Setting foreground and background colors independently lets each cell encode two pixel rows.

Combined with 24-bit ANSI escape sequences (ESC[38;2;R;G;Bm), cati achieves full true-color rendering with no image protocol required.

CharTop halfBottom half
fg colorbg color
bg colorfg color
fg colorfg color
transparenttransparent

24×14 source pixels → 24×7 terminal cells

spark overlay

Orig, half, quad, spark.

Compare the same samples across the full rendering ladder. The stack stays live, but the spark layer is the one doing the least work per cell.

2.0x

Drag any preview inside its frame to pan it.

soldering practice
Original soldering practice sample Half-block render of soldering practice sample at 80ch Quad-block render of soldering practice sample at 80ch Spark render of soldering practice sample at 80ch
summer vacation
Original summer vacation sample Half-block render of summer vacation sample at 50ch Quad-block render of summer vacation sample at 50ch Spark render of summer vacation sample at 50ch
darth daughter
Original darth daughter sample Half-block render of darth daughter sample at 50ch Quad-block render of darth daughter sample at 50ch Spark render of darth daughter sample at 50ch
usage

Simple by design

Show files or directories
$ cati photo.png
$ cati logo.svg
$ cati screenshots/
Browse directories
$ catibrowse folder/
$ cati browse folder/
$ cati browse file1.png file2.jpg
Play media
$ catiplay frames/
$ catiplay --fps 30 video.webm
$ cati play --fps 4 assets/
Compatibility forwards
$ cati play video.webm
$ cati browse folder/
$ cati --play assets/   # legacy
$ cati -i folder/      # legacy
install

Get started in seconds

Requires Go 1.25+

# install the commands
$ go install ubunatic.com/cati/cmd/cati@latest
$ go install ubunatic.com/cati/cmd/catiplay@latest
$ go install ubunatic.com/cati/cmd/catibrowse@latest

# try it
$ cati logo.svg
$ catiplay --fps 4 assets/
developer API

Use as a Go Library

Integrate high-performance terminal rendering directly into your Go TUIs

package main

import (
	"fmt"
	"image"
	"image/color"
	"os"

	"ubunatic.com/cati/v1/halfblock"
	"ubunatic.com/cati/v1/quadblock"
)

func main() {
	// Create a simple test image (a diagonal red line on blue background)
	img := image.NewRGBA(image.Rect(0, 0, 40, 40))
	for y := 0; y < 40; y++ {
		for x := 0; x < 40; x++ {
			if x == y {
				img.Set(x, y, color.RGBA{R: 255, G: 0, B: 0, A: 255})
			} else {
				img.Set(x, y, color.RGBA{R: 0, G: 0, B: 255, A: 255})
			}
		}
	}

	fmt.Println("--- Example 1: Rendering ANSI directly to Stdout ---")
	// Render using the halfblock algorithm at 20 terminal columns width.
	// Width is mandatory (20). Height is unconstrained (Opts.Rows = 0).
	err := halfblock.Render(os.Stdout, img, 20, halfblock.Options{})
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error rendering: %v\n", err)
		os.Exit(1)
	}

	fmt.Println("\n--- Example 2: Rendering to a core.Grid (for TUIs) ---")
	// Render using the quadblock algorithm with edge-snap enabled.
	opts := quadblock.Options{
		EdgeSnap: true,
	}
	grid, err := quadblock.RenderToGrid(img, 20, opts)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error rendering to grid: %v\n", err)
		os.Exit(1)
	}

	// Print out the grid cell runes (ignoring colors for simplicity in stdout)
	for y, row := range grid.Cells {
		fmt.Printf("Row %02d: ", y)
		for _, cell := range row {
			fmt.Printf("%c", cell.Ch)
		}
		fmt.Println()
	}
}