1
0
mirror of https://github.com/makew0rld/amfora.git synced 2024-12-04 14:46:29 -05:00
amfora/render/renderer.go

210 lines
5.5 KiB
Go
Raw Normal View History

package render
import (
"bufio"
"bytes"
"fmt"
"io"
"regexp"
2021-05-14 18:49:36 -04:00
"code.rocketnine.space/tslocum/cview"
"github.com/makeworld-the-better-one/amfora/config"
"github.com/spf13/viper"
)
// Renderer renderers network bytes into something that can be displayed on a
// cview.TextView.
//
// Calling Close when all writing is done is not a no-op, it will stop the the
2021-05-17 10:44:34 -04:00
// goroutine that runs for each Renderer, and will also allow the Links channel
// to be closed. Close should be called once all the data has been copied
//
// Write calls may block if the Lines channel buffer is full.
type Renderer interface {
io.ReadWriteCloser
2021-03-07 22:37:10 -05:00
// Links returns a channel that yields link URLs as they are parsed.
// It is buffered. The channel will be closed when there won't be anymore links.
Links() <-chan string
}
// ScanLines is copied from bufio.ScanLines and is used with bufio.Scanner.
// The only difference is that this func doesn't get rid of the end-of-line marker.
// This is so that the number of read bytes can be counted correctly in ReadFrom.
//
// It also simplifes code by no longer having to append a newline character.
func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\n'); i >= 0 {
// We have a full newline-terminated line.
return i + 1, data[0 : i+1], nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), data, nil
}
// Request more data.
return 0, nil, nil
}
// PlaintextRenderer escapes text for cview usage and does nothing else.
type PlaintextRenderer struct {
readOut *io.PipeReader
readIn *io.PipeWriter
writeIn *io.PipeWriter
writeOut *io.PipeReader
}
func NewPlaintextRenderer() *PlaintextRenderer {
pr1, pw1 := io.Pipe()
pr2, pw2 := io.Pipe()
ren := PlaintextRenderer{
readOut: pr1,
readIn: pw1,
writeIn: pw2,
writeOut: pr2,
}
go ren.handler()
return &ren
}
// handler is supposed to run in a goroutine as soon as the renderer is created.
// It handles the buffering and parsing in the background.
func (ren *PlaintextRenderer) handler() {
scanner := bufio.NewScanner(ren.writeOut)
scanner.Split(ScanLines)
for scanner.Scan() {
//nolint:errcheck
ren.readIn.Write(cview.EscapeBytes(scanner.Bytes()))
}
if err := scanner.Err(); err != nil {
// Close the ends this func touches, shouldn't matter really
ren.writeOut.CloseWithError(err)
ren.readIn.CloseWithError(err)
}
}
2021-03-04 20:14:56 -05:00
func (ren *PlaintextRenderer) Write(p []byte) (n int, err error) {
return ren.writeIn.Write(p)
}
2021-03-04 20:14:56 -05:00
func (ren *PlaintextRenderer) Read(p []byte) (n int, err error) {
return ren.readOut.Read(p)
}
func (ren *PlaintextRenderer) Close() error {
// Close user-facing ends of the pipes. Shouldn't matter which ends though
ren.writeIn.Close()
ren.readOut.Close()
return nil
}
2021-03-04 20:14:56 -05:00
func (ren *PlaintextRenderer) Links() <-chan string {
ch := make(chan string)
close(ch)
return ch
}
// ANSIRenderer escapes text for cview usage, as well as converting ANSI codes
// into cview tags if the config allows it.
type ANSIRenderer struct {
readOut *io.PipeReader
readIn *io.PipeWriter
writeIn *io.PipeWriter
writeOut *io.PipeReader
ansiWriter io.Writer // cview.ANSIWriter
buf *bytes.Buffer // Where ansiWriter writes to
}
// Regex for identifying ANSI color codes
var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`)
func NewANSIRenderer() *ANSIRenderer {
pr1, pw1 := io.Pipe()
pr2, pw2 := io.Pipe()
var ansiWriter io.Writer = nil // When ANSI is disabled
var buf bytes.Buffer
if viper.GetBool("a-general.color") && viper.GetBool("a-general.ansi") {
// ANSI enabled
ansiWriter = cview.ANSIWriter(&buf)
}
ren := ANSIRenderer{
readOut: pr1,
readIn: pw1,
writeIn: pw2,
writeOut: pr2,
ansiWriter: ansiWriter,
buf: &buf,
}
go ren.handler()
return &ren
}
// handler is supposed to run in a goroutine as soon as the renderer is created.
// It handles the buffering and parsing in the background.
func (ren *ANSIRenderer) handler() {
// Go through lines, render, and write each line
scanner := bufio.NewScanner(ren.writeOut)
scanner.Split(ScanLines)
for scanner.Scan() {
line := scanner.Bytes()
line = cview.EscapeBytes(line)
if ren.ansiWriter == nil {
// ANSI disabled
line = ansiRegex.ReplaceAll(scanner.Bytes(), nil)
} else {
// ANSI enabled
ren.buf.Reset()
// Shouldn't error because everything it writes to are all bytes.Buffer
ren.ansiWriter.Write(line) //nolint:errcheck
// The ANSIWriter injects tags like [-:-:-]
// but this will reset the background to use the user's terminal color.
// These tags need to be replaced with resets that use the theme color.
line = bytes.ReplaceAll(
ren.buf.Bytes(),
[]byte("[-:-:-]"),
[]byte(fmt.Sprintf("[-:%s:-]", config.GetColorString("bg"))),
)
}
ren.readIn.Write(line) //nolint:errcheck
}
if err := scanner.Err(); err != nil {
// Close the ends this func touches, shouldn't matter really
ren.writeOut.CloseWithError(err)
ren.readIn.CloseWithError(err)
}
}
func (ren *ANSIRenderer) Write(p []byte) (n int, err error) {
return ren.writeIn.Write(p)
}
2021-03-04 20:14:56 -05:00
func (ren *ANSIRenderer) Read(p []byte) (n int, err error) {
return ren.readOut.Read(p)
}
func (ren *ANSIRenderer) Close() error {
// Close user-facing ends of the pipes. Shouldn't matter which ends though
ren.writeIn.Close()
ren.readOut.Close()
return nil
}
2021-03-04 20:14:56 -05:00
func (ren *ANSIRenderer) Links() <-chan string {
ch := make(chan string)
close(ch)
return ch
}