Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions output.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,42 @@ func outputLineAsHTML(line screenLine) string {
}
return strings.TrimRight(lineBuf.buf.String(), " \t")
}

func (b *outputBuffer) appendANSIStyle(n node) {
for _, code := range n.style.asANSICodes() {
b.buf.Write([]byte(code))
}
}

func (b *outputBuffer) resetANSI() {
b.buf.Write([]byte("\u001b[0m"))
}

func outputLineAsANSI(line screenLine) string {
var styleApplied bool
var lineBuf outputBuffer

for idx, node := range line.nodes {
if idx == 0 && !node.style.isEmpty() {
lineBuf.appendANSIStyle(node)
styleApplied = true
} else if idx > 0 {
previous := line.nodes[idx-1]
if !node.hasSameStyle(previous) {
if styleApplied {
lineBuf.resetANSI()
styleApplied = false
}
if !node.style.isEmpty() {
lineBuf.appendANSIStyle(node)
styleApplied = true
}
}
}
lineBuf.buf.WriteRune(node.blob)
}
if styleApplied {
lineBuf.resetANSI()
}
return strings.TrimRight(lineBuf.buf.String(), " \t")
}
4 changes: 2 additions & 2 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const (
// Stateful ANSI parser
type parser struct {
mode int
screen *screen
screen *Screen
ansi []byte
cursor int
escapeStartedAt int
Expand Down Expand Up @@ -66,7 +66,7 @@ type parser struct {
* normally designate the character set.
*/

func parseANSIToScreen(s *screen, ansi []byte) {
func ParseANSIToScreen(s *Screen, ansi []byte) {
p := parser{mode: MODE_NORMAL, ansi: ansi, screen: s}
p.mode = MODE_NORMAL
length := len(p.ansi)
Expand Down
14 changes: 7 additions & 7 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ func TestParseXYAfterCursorMovementThroughBuildkiteTimestampAPC(t *testing.T) {

// ----------------------------------------

func parsedScreen(data string) *screen {
s := &screen{}
parseANSIToScreen(s, []byte(data))
func parsedScreen(data string) *Screen {
s := &Screen{}
ParseANSIToScreen(s, []byte(data))
return s
}

func assertXY(t *testing.T, s *screen, x, y int) error {
func assertXY(t *testing.T, s *Screen, x, y int) error {
if s.x != x {
return fmt.Errorf("expected screen.x == %d, got %d", x, s.x)
}
Expand All @@ -68,14 +68,14 @@ func assertXY(t *testing.T, s *screen, x, y int) error {
return nil
}

func assertText(t *testing.T, s *screen, expected string) error {
if actual := s.asPlainText(); actual != expected {
func assertText(t *testing.T, s *Screen, expected string) error {
if actual := s.AsPlainText(); actual != expected {
return fmt.Errorf("expected text %q, got %q", expected, actual)
}
return nil
}

func assertTextXY(t *testing.T, s *screen, expected string, x, y int) error {
func assertTextXY(t *testing.T, s *Screen, expected string, x, y int) error {
if err := assertXY(t, s, x, y); err != nil {
return err
}
Expand Down
75 changes: 52 additions & 23 deletions screen.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ import (
"strings"
)

// A terminal 'screen'. Current cursor position, cursor style, and characters
type screen struct {
func NewScreen() *Screen {
return &Screen{style: &emptyStyle}
}

// A terminal 'Screen'. Current cursor position, cursor style, and characters
type Screen struct {
x int
y int
screen []screenLine
Expand All @@ -27,7 +31,7 @@ const screenEndOfLine = -1
const screenStartOfLine = 0

// Clear part (or all) of a line on the screen
func (s *screen) clear(y int, xStart int, xEnd int) {
func (s *Screen) clear(y int, xStart int, xEnd int) {
if len(s.screen) <= y {
return
}
Expand Down Expand Up @@ -57,28 +61,28 @@ func ansiInt(s string) int {
}

// Move the cursor up, if we can
func (s *screen) up(i string) {
func (s *Screen) up(i string) {
s.y -= ansiInt(i)
s.y = int(math.Max(0, float64(s.y)))
}

// Move the cursor down
func (s *screen) down(i string) {
func (s *Screen) down(i string) {
s.y += ansiInt(i)
}

// Move the cursor forward on the line
func (s *screen) forward(i string) {
func (s *Screen) forward(i string) {
s.x += ansiInt(i)
}

// Move the cursor backward, if we can
func (s *screen) backward(i string) {
func (s *Screen) backward(i string) {
s.x -= ansiInt(i)
s.x = int(math.Max(0, float64(s.x)))
}

func (s *screen) getCurrentLineForWriting() *screenLine {
func (s *Screen) getCurrentLineForWriting() *screenLine {
// Add rows to our screen if necessary
for i := len(s.screen); i <= s.y; i++ {
s.screen = append(s.screen, screenLine{nodes: make([]node, 0, 80)})
Expand All @@ -94,33 +98,33 @@ func (s *screen) getCurrentLineForWriting() *screenLine {
}

// Write a character to the screen's current X&Y, along with the current screen style
func (s *screen) write(data rune) {
func (s *Screen) write(data rune) {
line := s.getCurrentLineForWriting()
line.nodes[s.x] = node{blob: data, style: s.style}
}

// Append a character to the screen
func (s *screen) append(data rune) {
func (s *Screen) append(data rune) {
s.write(data)
s.x++
}

// Append multiple characters to the screen
func (s *screen) appendMany(data []rune) {
func (s *Screen) appendMany(data []rune) {
for _, char := range data {
s.append(char)
}
}

func (s *screen) appendElement(i *element) {
func (s *Screen) appendElement(i *element) {
line := s.getCurrentLineForWriting()
line.nodes[s.x] = node{style: s.style, elem: i}
s.x++
}

// Set non-existing line metadata. Merges the provided data into any existing
// metadata for the current line, keeping existing data when keys collide.
func (s *screen) setnxLineMetadata(namespace string, data map[string]string) {
func (s *Screen) setnxLineMetadata(namespace string, data map[string]string) {
line := s.getCurrentLineForWriting()
if line.metadata == nil {
line.metadata = make(map[string]map[string]string)
Expand All @@ -139,12 +143,12 @@ func (s *screen) setnxLineMetadata(namespace string, data map[string]string) {
}

// Apply color instruction codes to the screen's current style
func (s *screen) color(i []string) {
func (s *Screen) color(i []string) {
s.style = s.style.color(i)
}

// Apply an escape sequence to the screen
func (s *screen) applyEscape(code rune, instructions []string) {
func (s *Screen) applyEscape(code rune, instructions []string) {
if len(instructions) == 0 {
// Ensure we always have a first instruction
instructions = []string{""}
Expand Down Expand Up @@ -205,13 +209,13 @@ func (s *screen) applyEscape(code rune, instructions []string) {
}

// Parse ANSI input, populate our screen buffer with nodes
func (s *screen) parse(ansi []byte) {
func (s *Screen) parse(ansi []byte) {
s.style = &emptyStyle

parseANSIToScreen(s, ansi)
ParseANSIToScreen(s, ansi)
}

func (s *screen) asHTML() []byte {
func (s *Screen) AsHTML() []byte {
var lines []string

for _, line := range s.screen {
Expand All @@ -222,7 +226,7 @@ func (s *screen) asHTML() []byte {
}

// asPlainText renders the screen without any ANSI style etc.
func (s *screen) asPlainText() string {
func (s *Screen) AsPlainText() string {
var buf bytes.Buffer
for i, line := range s.screen {
for _, node := range line.nodes {
Expand All @@ -237,23 +241,48 @@ func (s *screen) asPlainText() string {
return strings.TrimRight(buf.String(), " \t")
}

func (s *screen) newLine() {
func (s *Screen) AsANSI() []byte {
var lines []string

for _, line := range s.screen {
lines = append(lines, outputLineAsANSI(line))
}

return []byte(strings.Join(lines, "\n"))
}

func (s *Screen) newLine() {
s.x = 0
s.y++
}

func (s *screen) revNewLine() {
func (s *Screen) revNewLine() {
if s.y > 0 {
s.y--
}
}

func (s *screen) carriageReturn() {
func (s *Screen) carriageReturn() {
s.x = 0
}

func (s *screen) backspace() {
func (s *Screen) backspace() {
if s.x > 0 {
s.x--
}
}

func (s *Screen) FlushLinesFromTop(numLinesToRetain int) []byte {
numLinesToFlush := len(s.screen) - numLinesToRetain
if numLinesToFlush > s.y {
// log.Warningf("Screen attempted to pop line containing the current cursor position. Attempted to retain too few lines by %d line(s).", extraLines-s.y)
numLinesToFlush = s.y
}
if numLinesToFlush < 1 {
return []byte{}
}
flushedLines := (&Screen{screen: s.screen[:numLinesToFlush]}).AsANSI()
s.screen = s.screen[numLinesToFlush:]
s.y -= numLinesToFlush
return flushedLines
}
17 changes: 17 additions & 0 deletions style.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type style struct {
underline bool
strike bool
blink bool
colors *[]string
}

const (
Expand All @@ -30,6 +31,18 @@ func (s *style) isEqual(o *style) bool {
return s == o || *s == *o
}

// ANSI codes that make up the style
func (s *style) asANSICodes() []string {
var codes []string

if s.colors != nil {
for _, color := range *s.colors {
codes = append(codes, "\u001b["+color+"m")
}
}
return codes
}

// CSS classes that make up the style
func (s *style) asClasses() []string {
var styles []string
Expand Down Expand Up @@ -93,6 +106,10 @@ func (s *style) color(colors []string) *style {

newStyle := style(*s)
s = &newStyle
if s.colors == nil {
s.colors = &[]string{}
}
*s.colors = append(*s.colors, colors...)
color_mode := COLOR_NORMAL

for _, ccs := range colors {
Expand Down
4 changes: 2 additions & 2 deletions terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import "bytes"

// Render converts ANSI to HTML and returns the result.
func Render(input []byte) []byte {
screen := screen{}
screen := Screen{}
screen.parse(input)
output := bytes.Replace(screen.asHTML(), []byte("\n\n"), []byte("\n&nbsp;\n"), -1)
output := bytes.Replace(screen.AsHTML(), []byte("\n\n"), []byte("\n&nbsp;\n"), -1)
return output
}
4 changes: 2 additions & 2 deletions terminal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ func TestRendererAgainstFixtures(t *testing.T) {
}

func TestScreenWriteToXY(t *testing.T) {
s := screen{style: &emptyStyle}
s := Screen{style: &emptyStyle}
s.write('a')

s.x = 1
Expand All @@ -339,7 +339,7 @@ func TestScreenWriteToXY(t *testing.T) {
s.y = 2
s.write('c')

output := string(s.asHTML())
output := string(s.AsHTML())
expected := "a\n b\n c"
if output != expected {
t.Errorf("got %q, wanted %q", output, expected)
Expand Down