~mna/zzcsi

41fad45e13699404d283ec39ebfe1157e373beb0 — Martin Angers 1 year, 2 months ago
initial commit
8 files changed, 250 insertions(+), 0 deletions(-)

A .gitignore
A .golangci.toml
A LICENSE
A README.md
A csi.go
A csi_test.go
A doc.go
A go.mod
A  => .gitignore +11 -0
@@ 1,11 @@
# environment files
.env*

# compiled binary
/out 

# output of various helper commands
*.out

# binaries
/bin/

A  => .golangci.toml +30 -0
@@ 1,30 @@
[linters]
  disable-all = true
  enable = [
    "deadcode",
    "errcheck",
    "gochecknoinits",
    "gofmt",
    "golint",
    "gosec",
    "gosimple",
    "govet",
    "ineffassign",
    "misspell",
    "nakedret",
    "prealloc",
    "staticcheck",
    "structcheck",
    "typecheck",
    "unconvert",
    "unparam",
    "varcheck",
  ]

[issues]
  # regexps of issue texts to exclude
  # NOTE: using this instead of [[issues.exclude-rules]] as vim/ALE respects those
  # exclusions, but does not respect the specific exclude-rules(?).
  exclude = [
  ]


A  => LICENSE +11 -0
@@ 1,11 @@
Copyright (c) 2020, Martin Angers

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

A  => README.md +17 -0
@@ 1,17 @@
# zztermcsi [![builds.sr.ht status](https://builds.sr.ht/~mna/zztermcsi.svg)](https://builds.sr.ht/~mna/zztermcsi?) [![GoDoc](https://godoc.org/git.sr.ht/~mna/zztermcsi?status.svg)](http://godoc.org/git.sr.ht/~mna/zztermcsi) [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/git.sr.ht/~mna/zztermcsi)

Package zztermcsi [TODO ...]. See the [package documentation][godoc] for details,
API reference and usage example (alternatively, on [pkg.go.dev][pgd]).

* Canonical repository: https://git.sr.ht/~mna/zztermcsi
* Issues: https://todo.sr.ht/~mna/zztermcsi
* Builds: https://builds.sr.ht/~mna/zztermcsi

## License

The [BSD 3-Clause license][bsd].

[bsd]: http://opensource.org/licenses/BSD-3-Clause
[godoc]: http://godoc.org/git.sr.ht/~mna/zztermcsi
[pgd]: https://pkg.go.dev/git.sr.ht/~mna/zztermcsi


A  => csi.go +128 -0
@@ 1,128 @@
package zztermcsi

import (
	"bytes"
	"strconv"
	"strings"
)

// CSI represents a Control Sequence Introducer function as supported
// by xterm-compatible terminals.
//
// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
// for details.
type CSI byte

// List of CSI functions.
const (
	InsCh CSI = iota
	ShLeft
	CurUp
	ShRight
	CurDown
	CurFwd
	CurBwd
)

var (
	csiPrefix = []byte("\x1b[")

	// The CSI "Ps" (single number) parameter is encoded as "\x01" and the "Pm"
	// (multiple numbers separated by ;) is encoded as "\x02".

	insCh   = []byte("\x1b[\x01@")
	shLeft  = []byte("\x1b[\x01 @")
	curUp   = []byte("\x1b[\x01A")
	shRight = []byte("\x1b[\x01 A")
	curDown = []byte("\x1b[\x01B")
	curFwd  = []byte("\x1b[\x01C")
	curBwd  = []byte("\x1b[\x01D")
)

var csiSeqs = [...][]byte{
	InsCh:   insCh,
	ShLeft:  shLeft,
	CurUp:   curUp,
	ShRight: shRight,
	CurDown: curDown,
	CurFwd:  curFwd,
	CurBwd:  curBwd,
}

// Func returns the sequence of bytes to execute this CSI function with
// the provided numeric arguments. Note that no validation is done regarding
// the number of arguments - if the function supports a single argument, only
// one will be inserted, if it supports many, all expected arguments will be
// inserted. If less arguments than those expected are provided, the remaining
// arguments are left unspecified (which usually results in a default value
// fallback).
func (c CSI) Func(args ...int) []byte {
	if int(c) >= len(csiSeqs) {
		return nil
	}
	seq := csiSeqs[c]
	buf := make([]byte, 0, len(seq))
	return appendFunc(buf, seq, args)
}

// FuncString is like Func except it returns a string value. This can be useful
// to insert e.g. in a printf-style string.
func (c CSI) FuncString(args ...int) string {
	return string(c.Func(args...))
}

// AppendFunc is like Func except it appends the resulting sequence of bytes to
// b and returns the new slice. If b has a large enough capacity to hold the
// sequence, no allocation is made.
func (c CSI) AppendFunc(b []byte, args ...int) []byte {
	if int(c) >= len(csiSeqs) {
		return nil
	}
	seq := csiSeqs[c]
	return appendFunc(b, seq, args)
}

func appendFunc(buf, seq []byte, args []int) []byte {
	// start by processing the Pm (multiple numbers separated by ;), as there
	// can be only one placeholder if Pm is used.
	if ix := bytes.IndexByte(seq, '\x02'); ix >= 0 {
		buf = append(buf, seq[:ix]...)
		for i, arg := range args {
			if i > 0 {
				buf = append(buf, ';')
			}
			buf = strconv.AppendInt(buf, int64(arg), 10)
		}
		buf = append(buf, seq[ix+1:]...)
		return buf
	}

	// otherwise replace the Ps (single number) placeholders, there can be many.
	start := 0
	for start < len(seq) {
		ix := bytes.IndexByte(seq[start:], '\x01')
		if ix < 0 {
			buf = append(buf, seq[start:]...)
			break
		}
		buf = append(buf, seq[start:ix]...)
		start = ix + 1
		if len(args) > 0 {
			buf = strconv.AppendInt(buf, int64(args[0]), 10)
			args = args[1:]
		}
	}
	return buf
}

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

// IsCSIString returns true if s starts with the Control Sequence Introducer
// prefix (see IsCSI for details).
func IsCSIString(s string) bool {
	return strings.HasPrefix(s, string(csiPrefix))
}

A  => csi_test.go +49 -0
@@ 1,49 @@
package zztermcsi

import (
	"strings"
	"testing"
)

func TestFunc_NoArg(t *testing.T) {
	for i, seq := range csiSeqs {
		t.Run(string(seq), func(t *testing.T) {
			csi := CSI(i)
			want := strings.ReplaceAll(strings.ReplaceAll(string(seq), "\x01", ""), "\x02", "")
			got := csi.FuncString()
			if want != got {
				t.Fatalf("want %q, got %q", want, got)
			}
		})
	}
}

func TestIsCSI(t *testing.T) {
	for _, seq := range csiSeqs {
		t.Run(string(seq), func(t *testing.T) {
			if !IsCSI(seq) {
				t.Fatalf("sequence not detected as CSI: %q", seq)
			}
			if !IsCSIString(string(seq)) {
				t.Fatalf("string sequence not detected as CSI: %q", seq)
			}
		})

		// invert the sequence, should NOT be CSI
		iseq := make([]byte, len(seq))
		copy(iseq, seq)
		for i := len(iseq)/2 - 1; i >= 0; i-- {
			opp := len(iseq) - 1 - i
			iseq[i], iseq[opp] = iseq[opp], iseq[i]
		}

		t.Run(string(iseq), func(t *testing.T) {
			if IsCSI(iseq) {
				t.Fatalf("inverted sequence detected as CSI: %q", iseq)
			}
			if IsCSIString(string(iseq)) {
				t.Fatalf("inverted string sequence detected as CSI: %q", iseq)
			}
		})
	}
}

A  => doc.go +1 -0
@@ 1,1 @@
package zztermcsi // import "git.sr.ht/~mna/zztermcsi"

A  => go.mod +3 -0
@@ 1,3 @@
module git.sr.ht/~mna/zztermcsi

go 1.14