1// Command graphics-harness renders Kitty graphics terminal input to a PNG for
2// local development inspection. It is not a terminal emulator.
3package main
4
5import (
6 "flag"
7 "fmt"
8 "image"
9 "image/png"
10 "os"
11
12 vt "github.com/bnema/vev-vt"
13 "github.com/bnema/vev-vt/internal/graphicsharness"
14)
15
16func main() {
17 inputPath := flag.String("input", "", "path to a file containing raw terminal bytes")
18 outputPath := flag.String("output", "graphics-harness.png", "path for the rendered PNG")
19 cols := flag.Int("cols", 80, "terminal column count")
20 rows := flag.Int("rows", 24, "terminal row count")
21 pixelWidth := flag.Int("pixel-width", 640, "terminal viewport width in pixels")
22 pixelHeight := flag.Int("pixel-height", 384, "terminal viewport height in pixels")
23 scale := flag.Int("scale", 1, "nearest-neighbor output scale for inspection")
24 flag.Parse()
25
26 if *inputPath == "" {
27 fail("-input is required")
28 }
29 input, err := os.ReadFile(*inputPath)
30 if err != nil {
31 fail("read input: %v", err)
32 }
33 frame, err := graphicsharness.RenderTerminal(input, vt.Geometry{
34 Cols: *cols, Rows: *rows, PixelWidth: *pixelWidth, PixelHeight: *pixelHeight,
35 })
36 if err != nil {
37 fail("render input: %v", err)
38 }
39 frame, err = scaleFrame(frame, *scale)
40 if err != nil {
41 fail("scale output: %v", err)
42 }
43 output, err := os.Create(*outputPath)
44 if err != nil {
45 fail("create output: %v", err)
46 }
47 if err := png.Encode(output, frame); err != nil {
48 _ = output.Close()
49 fail("encode output: %v", err)
50 }
51 if err := output.Close(); err != nil {
52 fail("close output: %v", err)
53 }
54}
55
56func scaleFrame(source *image.RGBA, scale int) (*image.RGBA, error) {
57 if scale < 1 {
58 return nil, fmt.Errorf("scale must be positive")
59 }
60 width, height := source.Bounds().Dx(), source.Bounds().Dy()
61 maxInt := int(^uint(0) >> 1)
62 if width > 0 && scale > maxInt/width {
63 return nil, fmt.Errorf("scaled frame exceeds 67108864 pixels")
64 }
65 if height > 0 && scale > maxInt/height {
66 return nil, fmt.Errorf("scaled frame exceeds 67108864 pixels")
67 }
68 scaledWidth, scaledHeight := width*scale, height*scale
69 const maxScaledPixels = uint64(64 << 20)
70 if scaledWidth > 0 && uint64(scaledHeight) > maxScaledPixels/uint64(scaledWidth) {
71 return nil, fmt.Errorf("scaled frame exceeds 67108864 pixels")
72 }
73 result := image.NewRGBA(image.Rect(0, 0, scaledWidth, scaledHeight))
74 for y := range height {
75 for x := range width {
76 pixel := source.RGBAAt(x, y)
77 for dy := range scale {
78 for dx := range scale {
79 result.SetRGBA(x*scale+dx, y*scale+dy, pixel)
80 }
81 }
82 }
83 }
84 return result, nil
85}
86
87func fail(format string, args ...any) {
88 fmt.Fprintf(os.Stderr, "graphics-harness: "+format+"\n", args...)
89 os.Exit(1)
90}