~mna/zzcsi

3a39bd0ae47116c1f07f38f8a2bbcacd25eabdaa — Martin Angers 1 year, 2 months ago 6bff928 master v0.1.0
add decode args, test, benchmark, example
4 files changed, 153 insertions(+), 3 deletions(-)

M README.md
M csi.go
M csi_test.go
M example_test.go
M README.md => README.md +4 -3
@@ 25,9 25,10 @@ CSI escape sequence without allocation.
```
benchmark                       iter      time/iter   bytes alloc        allocs
---------                       ----      ---------   -----------        ------
BenchmarkCSI/Func-4          9259478   118.00 ns/op       24 B/op   2 allocs/op
BenchmarkCSI/FuncString-4    9003938   133.00 ns/op       32 B/op   3 allocs/op
BenchmarkCSI/AppendFunc-4   20808049    58.40 ns/op        0 B/op   0 allocs/op
BenchmarkCSI/Func-4          9971707   116.00 ns/op       24 B/op   2 allocs/op
BenchmarkCSI/FuncString-4    8935776   135.00 ns/op       32 B/op   3 allocs/op
BenchmarkCSI/AppendFunc-4   20534767    58.80 ns/op        0 B/op   0 allocs/op
BenchmarkCSI/DecodeArgs-4   20537282    55.00 ns/op        0 B/op   0 allocs/op
```

## License

M csi.go => csi.go +60 -0
@@ 758,6 758,66 @@ func appendFunc(buf, seq []byte, args []int) []byte {
	return buf
}

// DecodeArgs decodes the numerical arguments of an escape sequence into the
// integer points provided in dst. It returns the number of arguments decoded,
// which is <= len(dst). It starts decoding arguments at the first byte that
// is a decimal digit, and continues decoding numbers as long as they are
// separated by semicolons.
//
// This is useful for some csi functions that trigger a reply from the terminal.
// Such a reply can be read e.g. with git.sr.ht/~mna/zzterm (which would read it
// as a key of type KeyESCSeq, and then the Input.Bytes can be passed as b to
// this function to get the arguments). A common example of such a reply is for
// the DevStat CSI function when DevStatCurPos is requested (the cursor position).
// The terminal replies with "CSI r ; c R" where "r" is the row and "c" the column.
// DecodeArgs can be used to get the row and column values from the reply bytes.
func DecodeArgs(b []byte, dst ...*uint64) int {
	if len(b) == 0 || len(dst) == 0 {
		return 0
	}
	start := bytes.IndexAny(b, "0123456789")
	if start < 0 {
		return 0
	}

	var count int
	b = b[start:]
	for _, d := range dst {
		v, nb := decodeArg(b)
		*d = v
		b = b[nb:]
		count++

		// continue only if there are still bytes and the current is a semicolon
		if len(b) == 0 || b[0] != ';' {
			return count
		}

		next := bytes.IndexAny(b, "0123456789")
		if next != 1 { // 0 being the semicolon
			return count
		}
		b = b[next:]
	}
	return count
}

// decodes the number at the start of b, returns the number decoded and
// the number of bytes used for that number.
func decodeArg(b []byte) (uint64, int) {
	var v uint64
	for i, ch := range b {
		switch ch {
		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
			v *= 10
			v += uint64(ch - '0')
		default:
			return v, i
		}
	}
	return v, len(b)
}

// IsCSI returns true if b starts with the Control Sequence Introducer
// bytes ("\x1b[", or <ESC> followed by '[').
func IsCSI(b []byte) bool {

M csi_test.go => csi_test.go +54 -0
@@ 55,6 55,46 @@ func TestFunc_Args(t *testing.T) {
	}
}

func TestDecodeArgs(t *testing.T) {
	cases := []struct {
		in    string
		want  []uint64
		count int
	}{
		{"\x1b[0n", nil, 0},
		{"\x1b[0n", []uint64{0}, 1},
		{"\x1b[0n", []uint64{0, 0, 0}, 1},
		{"\x1b[0;1n", []uint64{0, 1, 0}, 2},
		{"\x1b[0;1;2n", []uint64{0, 1, 2}, 3},
		{"\x1b[0;1;2;3n", []uint64{0, 1, 2}, 3},
		{"\x1b[127n", []uint64{127, 0, 0}, 1},
		{"\x1b[127;87650n", []uint64{127, 87650, 0}, 2},
		{"\x1b[127; 1", []uint64{127, 0}, 1},
		{"\x1b[1;2;3", nil, 0},
		{"\x1b[1;2;3p", []uint64{1}, 1},
	}
	for _, c := range cases {
		t.Run(c.in, func(t *testing.T) {
			args := make([]*uint64, len(c.want))
			for i := range args {
				args[i] = new(uint64)
			}

			n := DecodeArgs([]byte(c.in), args...)
			if n != c.count {
				t.Fatalf("want %d args, got %d", c.count, n)
			}

			for j := 0; j < n; j++ {
				want, got := c.want[j], *args[j]
				if want != got {
					t.Fatalf("%d: want %d, got %d", j, want, got)
				}
			}
		})
	}
}

func TestIsCSI(t *testing.T) {
	for _, seq := range csiSeqs {
		if len(seq) == 0 {


@@ 92,6 132,7 @@ func TestIsCSI(t *testing.T) {
var (
	BenchmarkResultString string
	BenchmarkResultBytes  []byte
	BenchmarkResultInt    int
)

func BenchmarkCSI(b *testing.B) {


@@ 114,4 155,17 @@ func BenchmarkCSI(b *testing.B) {
			BenchmarkResultBytes = ChLnCol.AppendFunc(buf, 12, 80)
		}
	})

	b.Run("DecodeArgs", func(b *testing.B) {
		var r, c uint64
		seq := []byte("\x1b[12;33R")
		b.ResetTimer()

		for i := 0; i < b.N; i++ {
			BenchmarkResultInt = DecodeArgs(seq, &r, &c)
			if r != 12 || c != 33 {
				b.Fatalf("got %d, %d", r, c)
			}
		}
	})
}

M example_test.go => example_test.go +35 -0
@@ 2,6 2,7 @@ package zzcsi_test

import (
	"fmt"
	"io"

	"git.sr.ht/~mna/zzcsi"
)


@@ 13,3 14,37 @@ func ExampleCSI_FuncString() {
	defNoBlink := zzcsi.ChAttr.FuncString(zzcsi.ChAttrFgDef, zzcsi.ChAttrNoBlink)
	fmt.Printf("%sHello%s, %sworld!%s\n", bold, norm, blueBlink, defNoBlink)
}

func ExampleDecodeArgs() {
	// Let's pretend that term is a terminal in raw mode
	var term io.ReadWriter

	// a typical usage would be to send a CSI function to get the cursor
	// position:
	//
	fn := zzcsi.DevStat.Func(zzcsi.DevStatCurPos)
	fmt.Fprint(term, fn)
	//
	// and then get the reply using the zzterm package, e.g.:
	//
	//  for {
	//  	k, err := input.ReadKey(term)
	//  	if err != nil {
	//  		log.Panic(err)
	//  	}
	//
	//  	switch k.Type() {
	//    case zzterm.KeyESCSeq:
	//      b := input.Bytes()
	//  	}
	//  }
	//
	// Let's pretend that's how we got that reply byte sequence:
	b := []byte("\x1b[12;33R")
	var row, col uint64
	n := zzcsi.DecodeArgs(b, &row, &col)
	if n != 2 {
		// oops, did not get both arguments, fail
	}
	// otherwise use row and col
}