Skip to content
Draft
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
185 changes: 102 additions & 83 deletions aec.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package aec

import "fmt"
import "strconv"

// EraseMode is listed in a variable EraseModes.
type EraseMode uint

var (
// EraseModes is a list of EraseMode.
EraseModes struct {
EraseModes = struct {
// All erase all.
All EraseMode

Expand All @@ -16,124 +16,143 @@ var (

// Tail erase to tail.
Tail EraseMode
}{
Tail: 0,
Head: 1,
All: 2,
}

// Save saves the cursor position.
Save ANSI
// Save saves the cursor position. It uses both SCO ("ESC[s") and DEC
// ("ESC7") sequences as those were never standardized as part of the
// ANSI.
Save ANSI = newAnsi(esc + "s" + "\x1b7")

// Restore restores the cursor position.
Restore ANSI
// Restore restores the cursor position. It uses both SCO ("ESC[u") and
// DEC ("ESC8") sequences as those were never standardized as part of
// the ANSI.
Restore ANSI = newAnsi(esc + "u" + "\x1b8")

// Hide hides the cursor.
Hide ANSI
Hide ANSI = newAnsi(esc + "?25l")

// Show shows the cursor.
Show ANSI
Show ANSI = newAnsi(esc + "?25h")

// Report reports the cursor position.
Report ANSI
Report ANSI = newAnsi(esc + "6n")
)

// Up moves up the cursor.
func Up(n uint) ANSI {
if n == 0 {
return empty
}
return newAnsi(fmt.Sprintf(esc+"%dA", n))
}
// Up moves the cursor up by n positions (CUU).
func Up(n uint) ANSI { return csiUintDefault1(n, 'A') }

// Down moves down the cursor.
func Down(n uint) ANSI {
if n == 0 {
return empty
}
return newAnsi(fmt.Sprintf(esc+"%dB", n))
}
// Down moves the cursor down by n positions (CUD).
func Down(n uint) ANSI { return csiUintDefault1(n, 'B') }

// Right moves right the cursor.
func Right(n uint) ANSI {
if n == 0 {
return empty
}
return newAnsi(fmt.Sprintf(esc+"%dC", n))
}
// Right moves the cursor right by n positions (CUF).
func Right(n uint) ANSI { return csiUintDefault1(n, 'C') }

// Left moves left the cursor.
func Left(n uint) ANSI {
if n == 0 {
return empty
}
return newAnsi(fmt.Sprintf(esc+"%dD", n))
}
// Left moves the cursor left by n positions (CUB).
func Left(n uint) ANSI { return csiUintDefault1(n, 'D') }

// NextLine moves down the cursor to head of a line.
func NextLine(n uint) ANSI {
if n == 0 {
return empty
}
return newAnsi(fmt.Sprintf(esc+"%dE", n))
}
// NextLine moves the cursor down n lines and to the beginning of the line (CNL).
func NextLine(n uint) ANSI { return csiUintDefault1(n, 'E') }

// PreviousLine moves up the cursor to head of a line.
func PreviousLine(n uint) ANSI {
if n == 0 {
return empty
}
return newAnsi(fmt.Sprintf(esc+"%dF", n))
}
// PreviousLine moves the cursor up n lines and to the beginning of the line (CPL).
func PreviousLine(n uint) ANSI { return csiUintDefault1(n, 'F') }

// Column set the cursor position to a given column.
// Column sets the cursor position to a given column (CHA).
func Column(col uint) ANSI {
return newAnsi(fmt.Sprintf(esc+"%dG", col))
if col <= 1 {
return newAnsi(esc + "G")
}
return csiUint(col, 'G')
}

// Position set the cursor position to a given absolute position.
// Position sets the cursor position to a given absolute position (CUP).
func Position(row, col uint) ANSI {
return newAnsi(fmt.Sprintf(esc+"%d;%dH", row, col))
// According to ECMA-48 (ISO/IEC 6429), cursor positioning (CUP) uses 1-based
// coordinates with default values of 1;1. Empty or zero parameters are treated
// as defaults, so 0;0 is effectively interpreted as 1;1 (home position).
if row == 0 {
row = 1
}
if col == 0 {
col = 1
}
if row == 1 && col == 1 {
// ESC[0;0H == ESC[1;1H == ESC[H
return newAnsi(esc + "H")
}
buf := make([]byte, 0, len(esc)+42)
buf = append(buf, esc...)
buf = strconv.AppendUint(buf, uint64(row), 10)
buf = append(buf, ';')
buf = strconv.AppendUint(buf, uint64(col), 10)
buf = append(buf, 'H')
return newAnsi(string(buf))
}

// EraseDisplay erases display by given EraseMode.
// EraseDisplay erases the display using the given EraseMode (ED).
func EraseDisplay(m EraseMode) ANSI {
return newAnsi(fmt.Sprintf(esc+"%dJ", m))
if m == 0 {
return newAnsi(esc + "J")
}
return csiUint(uint(m), 'J')
}

// EraseLine erases lines by given EraseMode.
// EraseLine erases the line using the given EraseMode (EL).
func EraseLine(m EraseMode) ANSI {
return newAnsi(fmt.Sprintf(esc+"%dK", m))
if m == 0 {
return newAnsi(esc + "K")
}
return csiUint(uint(m), 'K')
}

// ScrollUp scrolls up the page.
// ScrollUp scrolls the display up by n lines (SU). n <= 0 is a no-op.
func ScrollUp(n int) ANSI {
if n == 0 {
return empty
}
return newAnsi(fmt.Sprintf(esc+"%dS", n))
return scroll(n, 'S')
}

// ScrollDown scrolls down the page.
// ScrollDown scrolls the display down by n lines (SD). n <= 0 is a no-op.
func ScrollDown(n int) ANSI {
if n == 0 {
return scroll(n, 'T')
}

func scroll(n int, direction byte) ANSI {
switch {
case n <= 0:
// Negative counts are not defined for SU/SD; treat them as a no-op.
return empty
case n == 1:
return newAnsi(esc + string(direction))
default:
return csiInt(n, direction)
}
return newAnsi(fmt.Sprintf(esc+"%dT", n))
}

func init() {
EraseModes = struct {
All EraseMode
Head EraseMode
Tail EraseMode
}{
Tail: 0,
Head: 1,
All: 2,
func csiInt(n int, suffix byte) ANSI {
buf := make([]byte, 0, len(esc)+21)
buf = append(buf, esc...)
buf = strconv.AppendInt(buf, int64(n), 10)
buf = append(buf, suffix)
return newAnsi(string(buf))
}

func csiUintDefault1(n uint, direction byte) ANSI {
switch n {
case 0:
return empty
case 1:
return newAnsi(esc + string(direction))
default:
return csiUint(n, direction)
}
}

// Save use both SCO (ESC[s) and DEC (ESC7) sequences as those were never standardised as part of the ANSI
Save = newAnsi(esc + "s" + "\x1b7")
// Restore use both SCO (ESC[u) and DEC (ESC8) and DEC sequences as those were never standardised as part of the ANSI
Restore = newAnsi(esc + "u" + "\x1b8")
Hide = newAnsi(esc + "?25l")
Show = newAnsi(esc + "?25h")
Report = newAnsi(esc + "6n")
func csiUint(n uint, suffix byte) ANSI {
buf := make([]byte, 0, len(esc)+21)
buf = append(buf, esc...)
buf = strconv.AppendUint(buf, uint64(n), 10)
buf = append(buf, suffix)
return newAnsi(string(buf))
}
71 changes: 71 additions & 0 deletions aec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package aec_test

import (
"fmt"
"testing"

"github.com/morikuni/aec"
)

var benchValues = []struct {
n uint
}{
{n: 0}, // 0,1 special case
{n: 2}, // 1 digit
{n: 20}, // 2 digit
{n: 200}, // 3 digits
}

// Representative benchmark for all single-parameter uint CSI generators:
// Up, Down, Left, Right, NextLine, PreviousLine, Column, EraseDisplay, and EraseLine.
func BenchmarkCursor(b *testing.B) {
for _, tc := range benchValues {
b.Run(fmt.Sprintf("%d", tc.n), func(b *testing.B) {
var a aec.ANSI
b.ReportAllocs()
for i := 0; i < b.N; i++ {
a = aec.Up(tc.n)
}
sinkANSI = a
})
}
}

// Covers the two-parameter uint CSI path used by [aec.Position].
func BenchmarkPosition(b *testing.B) {
var tests = []struct {
row uint
col uint
}{
{row: 0, col: 0}, // 0,1 special case
{row: 2, col: 2}, // 1 digit
{row: 20, col: 20}, // 2 digit
{row: 200, col: 200}, // 3 digits
{row: 5, col: 15},
}
for _, tc := range tests {
b.Run(fmt.Sprintf("%d,%d", tc.row, tc.col), func(b *testing.B) {
var a aec.ANSI
b.ReportAllocs()
for i := 0; i < b.N; i++ {
a = aec.Position(tc.row, tc.col)
}
sinkANSI = a
})
}
}

// Representative benchmark for all single-parameter int CSI
// generators ([aec.ScrollUp], [aec.ScrollDown]).
func BenchmarkScrollUpDown(b *testing.B) {
for _, tc := range benchValues {
b.Run(fmt.Sprintf("%d", tc.n), func(b *testing.B) {
var a aec.ANSI
b.ReportAllocs()
for i := 0; i < b.N; i++ {
a = aec.ScrollUp(int(tc.n))
}
sinkANSI = a
})
}
}
9 changes: 7 additions & 2 deletions ansi.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
const esc = "\x1b["

// Reset resets SGR effect.
const Reset string = "\x1b[0m"
const Reset = "\x1b[0m"

var empty = newAnsi("")

Expand All @@ -23,33 +23,38 @@ type ANSI interface {
Apply(string) string
}

// ansiImpl represents an ANSI escape code.
type ansiImpl string

func newAnsi(s string) *ansiImpl {
r := ansiImpl(s)
return &r
}

// With returns a new ANSICode sequence composed of this and the provided ANSI codes.
func (a *ansiImpl) With(ansi ...ANSI) ANSI {
return concat(append([]ANSI{a}, ansi...))
}

// Apply wraps the given string with the ANSI sequence and a reset code.
func (a *ansiImpl) Apply(s string) string {
return a.String() + s + Reset
}

// String returns the ANSICode escape code as a string.
func (a *ansiImpl) String() string {
return string(*a)
}

// Apply wraps given string in ANSIs.
// Apply wraps the given string with all provided ANSI sequences.
func Apply(s string, ansi ...ANSI) string {
if len(ansi) == 0 {
return s
}
return concat(ansi).Apply(s)
}

// concat combines multiple ANSI codes into a single ANSI sequence.
func concat(ansi []ANSI) ANSI {
strs := make([]string, 0, len(ansi))
for _, p := range ansi {
Expand Down
6 changes: 1 addition & 5 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type Builder struct {
}

// EmptyBuilder is an initialized Builder.
var EmptyBuilder *Builder
var EmptyBuilder = &Builder{empty}

// NewBuilder creates a Builder from existing ANSI.
func NewBuilder(a ...ANSI) *Builder {
Expand Down Expand Up @@ -382,7 +382,3 @@ func (builder *Builder) RGB8BitF(r, g, b uint8) *Builder {
func (builder *Builder) RGB8BitB(r, g, b uint8) *Builder {
return builder.Color8BitB(NewRGB8Bit(r, g, b))
}

func init() {
EmptyBuilder = &Builder{empty}
}
Loading