aboutsummaryrefslogtreecommitdiffstats
path: root/internal/render/render.go
blob: 18708f31aedd8f6252d15f3851e53e8ea93064f6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// Package render renders to the terminal using ANSI escape sequences.
package render

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
	"unicode/utf8"
)

// Renderer updates a terminal UI. Typical usage looks like
//
//	r := render.New()
//	// Game loop
//	for {
//	    // Update state
//
//	    r.Clear()
//	    fmt.Fprintf(r, "Render UI by writing to r using io.Writer")
//	    r.Flush()
//	}
type Renderer struct {
	w              bufio.Writer
	prevLines      int
	width          int
	partialLineLen int
}

// New creates a new Renderer
func New() *Renderer {
	r := &Renderer{}
	r.w.Reset(os.Stderr)
	return r
}

// Clear clears the screen before rendering a new frame.
func (r *Renderer) Clear(width int) {
	r.width = width
	if r.prevLines > 0 {
		fmt.Fprintf(&r.w, "\033[%dA", r.prevLines)
	}
	r.w.WriteString("\r")
	r.prevLines = 0
	r.partialLineLen = 0
}

func truncate(b []byte, width int) ([]byte, int) {
	n := 0
	for i := 0; i < len(b); {
		if bytes.HasPrefix(b[i:], []byte("\033[")) {
			// An escape sequence usually starts with [, then has one or two numbers
			// separated by semicolon, and ends with some terminating character. To
			// try and munch the whole sequence, skip over any numbers and
			// semicolon here.
			for i += 2; i < len(b)-1 && ('0' <= b[i] && b[i] <= '9' || b[i] == ';'); i++ {
			}
			// Skip the terminating character.
			i++
			continue
		}
		if n+1 > width {
			return b[:i], n
		}
		_, runeWidth := utf8.DecodeRune(b[i:])
		i += runeWidth
		n++
	}
	return b, n
}

// Write implements io.Writer.
func (r *Renderer) Write(buf []byte) (int, error) {
	totalBytes := 0
	for len(buf) > 0 {
		i := bytes.IndexByte(buf, '\n')
		if i < 0 {
			line, lineWidth := truncate(buf, r.width-r.partialLineLen)
			r.partialLineLen += lineWidth
			if n, err := r.w.Write(line); err != nil {
				return totalBytes + n, err
			}
			totalBytes += len(buf)
			return totalBytes, nil
		}
		line, _ := truncate(buf[:i], r.width)
		buf = buf[i+1:]
		if n, err := r.w.Write(line); err != nil {
			return totalBytes + n, err
		}
		totalBytes += i
		if _, err := r.w.WriteString("\033[K\n"); err != nil {
			return totalBytes, err
		}
		totalBytes++
		r.prevLines++
		r.partialLineLen = 0
	}
	return totalBytes, nil
}

// Flush flushes the internal buffer to stdout. Flush should be called at the
// end of every frame.
func (r *Renderer) Flush() {
	r.w.WriteString("\033[J")
	r.w.Flush()
}