chore: vendor

This commit is contained in:
2024-08-04 11:06:58 +02:00
parent 2a5985e44e
commit 04aec8232f
3557 changed files with 981078 additions and 1 deletions

View File

@ -0,0 +1,22 @@
Copyright (c) 2014 Takashi Kokubun
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,3 @@
# survey/terminal
This package started as a copy of [kokuban/go-ansi](http://github.com/k0kubun/go-ansi) but has since been modified to fit survey's specific needs.

View File

@ -0,0 +1,22 @@
package terminal
import (
"bytes"
"io"
)
type BufferedReader struct {
In io.Reader
Buffer *bytes.Buffer
}
func (br *BufferedReader) Read(p []byte) (int, error) {
n, err := br.Buffer.Read(p)
if err != nil && err != io.EOF {
return n, err
} else if err == nil {
return n, nil
}
return br.In.Read(p[n:])
}

View File

@ -0,0 +1,209 @@
//go:build !windows
// +build !windows
package terminal
import (
"bufio"
"bytes"
"fmt"
"io"
"regexp"
"strconv"
)
var COORDINATE_SYSTEM_BEGIN Short = 1
var dsrPattern = regexp.MustCompile(`\x1b\[(\d+);(\d+)R$`)
type Cursor struct {
In FileReader
Out FileWriter
}
// Up moves the cursor n cells to up.
func (c *Cursor) Up(n int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dA", n)
return err
}
// Down moves the cursor n cells to down.
func (c *Cursor) Down(n int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dB", n)
return err
}
// Forward moves the cursor n cells to right.
func (c *Cursor) Forward(n int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dC", n)
return err
}
// Back moves the cursor n cells to left.
func (c *Cursor) Back(n int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dD", n)
return err
}
// NextLine moves cursor to beginning of the line n lines down.
func (c *Cursor) NextLine(n int) error {
if err := c.Down(1); err != nil {
return err
}
return c.HorizontalAbsolute(0)
}
// PreviousLine moves cursor to beginning of the line n lines up.
func (c *Cursor) PreviousLine(n int) error {
if err := c.Up(1); err != nil {
return err
}
return c.HorizontalAbsolute(0)
}
// HorizontalAbsolute moves cursor horizontally to x.
func (c *Cursor) HorizontalAbsolute(x int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%dG", x)
return err
}
// Show shows the cursor.
func (c *Cursor) Show() error {
_, err := fmt.Fprint(c.Out, "\x1b[?25h")
return err
}
// Hide hide the cursor.
func (c *Cursor) Hide() error {
_, err := fmt.Fprint(c.Out, "\x1b[?25l")
return err
}
// move moves the cursor to a specific x,y location.
func (c *Cursor) move(x int, y int) error {
_, err := fmt.Fprintf(c.Out, "\x1b[%d;%df", x, y)
return err
}
// Save saves the current position
func (c *Cursor) Save() error {
_, err := fmt.Fprint(c.Out, "\x1b7")
return err
}
// Restore restores the saved position of the cursor
func (c *Cursor) Restore() error {
_, err := fmt.Fprint(c.Out, "\x1b8")
return err
}
// for comparability purposes between windows
// in unix we need to print out a new line on some terminals
func (c *Cursor) MoveNextLine(cur *Coord, terminalSize *Coord) error {
if cur.Y == terminalSize.Y {
if _, err := fmt.Fprintln(c.Out); err != nil {
return err
}
}
return c.NextLine(1)
}
// Location returns the current location of the cursor in the terminal
func (c *Cursor) Location(buf *bytes.Buffer) (*Coord, error) {
// ANSI escape sequence for DSR - Device Status Report
// https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
if _, err := fmt.Fprint(c.Out, "\x1b[6n"); err != nil {
return nil, err
}
// There may be input in Stdin prior to CursorLocation so make sure we don't
// drop those bytes.
var loc []int
var match string
for loc == nil {
// Reports the cursor position (CPR) to the application as (as though typed at
// the keyboard) ESC[n;mR, where n is the row and m is the column.
reader := bufio.NewReader(c.In)
text, err := reader.ReadSlice(byte('R'))
if err != nil {
return nil, err
}
loc = dsrPattern.FindStringIndex(string(text))
if loc == nil {
// After reading slice to byte 'R', the bufio Reader may have read more
// bytes into its internal buffer which will be discarded on next ReadSlice.
// We create a temporary buffer to read the remaining buffered slice and
// write them to output buffer.
buffered := make([]byte, reader.Buffered())
_, err = io.ReadFull(reader, buffered)
if err != nil {
return nil, err
}
// Stdin contains R that doesn't match DSR, so pass the bytes along to
// output buffer.
buf.Write(text)
buf.Write(buffered)
} else {
// Write the non-matching leading bytes to output buffer.
buf.Write(text[:loc[0]])
// Save the matching bytes to extract the row and column of the cursor.
match = string(text[loc[0]:loc[1]])
}
}
matches := dsrPattern.FindStringSubmatch(string(match))
if len(matches) != 3 {
return nil, fmt.Errorf("incorrect number of matches: %d", len(matches))
}
col, err := strconv.Atoi(matches[2])
if err != nil {
return nil, err
}
row, err := strconv.Atoi(matches[1])
if err != nil {
return nil, err
}
return &Coord{Short(col), Short(row)}, nil
}
func (cur Coord) CursorIsAtLineEnd(size *Coord) bool {
return cur.X == size.X
}
func (cur Coord) CursorIsAtLineBegin() bool {
return cur.X == COORDINATE_SYSTEM_BEGIN
}
// Size returns the height and width of the terminal.
func (c *Cursor) Size(buf *bytes.Buffer) (*Coord, error) {
// the general approach here is to move the cursor to the very bottom
// of the terminal, ask for the current location and then move the
// cursor back where we started
// hide the cursor (so it doesn't blink when getting the size of the terminal)
c.Hide()
defer c.Show()
// save the current location of the cursor
c.Save()
defer c.Restore()
// move the cursor to the very bottom of the terminal
c.move(999, 999)
// ask for the current location
bottom, err := c.Location(buf)
if err != nil {
return nil, err
}
// since the bottom was calculated in the lower right corner, it
// is the dimensions we are looking for
return bottom, nil
}

View File

@ -0,0 +1,164 @@
package terminal
import (
"bytes"
"syscall"
"unsafe"
)
var COORDINATE_SYSTEM_BEGIN Short = 0
// shared variable to save the cursor location from CursorSave()
var cursorLoc Coord
type Cursor struct {
In FileReader
Out FileWriter
}
func (c *Cursor) Up(n int) error {
return c.cursorMove(0, n)
}
func (c *Cursor) Down(n int) error {
return c.cursorMove(0, -1*n)
}
func (c *Cursor) Forward(n int) error {
return c.cursorMove(n, 0)
}
func (c *Cursor) Back(n int) error {
return c.cursorMove(-1*n, 0)
}
// save the cursor location
func (c *Cursor) Save() error {
loc, err := c.Location(nil)
if err != nil {
return err
}
cursorLoc = *loc
return nil
}
func (c *Cursor) Restore() error {
handle := syscall.Handle(c.Out.Fd())
// restore it to the original position
_, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursorLoc))))
return normalizeError(err)
}
func (cur Coord) CursorIsAtLineEnd(size *Coord) bool {
return cur.X == size.X
}
func (cur Coord) CursorIsAtLineBegin() bool {
return cur.X == 0
}
func (c *Cursor) cursorMove(x int, y int) error {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return err
}
var cursor Coord
cursor.X = csbi.cursorPosition.X + Short(x)
cursor.Y = csbi.cursorPosition.Y + Short(y)
_, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
return normalizeError(err)
}
func (c *Cursor) NextLine(n int) error {
if err := c.Up(n); err != nil {
return err
}
return c.HorizontalAbsolute(0)
}
func (c *Cursor) PreviousLine(n int) error {
if err := c.Down(n); err != nil {
return err
}
return c.HorizontalAbsolute(0)
}
// for comparability purposes between windows
// in windows we don't have to print out a new line
func (c *Cursor) MoveNextLine(cur *Coord, terminalSize *Coord) error {
return c.NextLine(1)
}
func (c *Cursor) HorizontalAbsolute(x int) error {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return err
}
var cursor Coord
cursor.X = Short(x)
cursor.Y = csbi.cursorPosition.Y
if csbi.size.X < cursor.X {
cursor.X = csbi.size.X
}
_, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
return normalizeError(err)
}
func (c *Cursor) Show() error {
handle := syscall.Handle(c.Out.Fd())
var cci consoleCursorInfo
if _, _, err := procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))); normalizeError(err) != nil {
return err
}
cci.visible = 1
_, _, err := procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
return normalizeError(err)
}
func (c *Cursor) Hide() error {
handle := syscall.Handle(c.Out.Fd())
var cci consoleCursorInfo
if _, _, err := procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))); normalizeError(err) != nil {
return err
}
cci.visible = 0
_, _, err := procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
return normalizeError(err)
}
func (c *Cursor) Location(buf *bytes.Buffer) (*Coord, error) {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return nil, err
}
return &csbi.cursorPosition, nil
}
func (c *Cursor) Size(buf *bytes.Buffer) (*Coord, error) {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return nil, err
}
// windows' coordinate system begins at (0, 0)
csbi.size.X--
csbi.size.Y--
return &csbi.size, nil
}

View File

@ -0,0 +1,9 @@
package terminal
type EraseLineMode int
const (
ERASE_LINE_END EraseLineMode = iota
ERASE_LINE_START
ERASE_LINE_ALL
)

View File

@ -0,0 +1,13 @@
//go:build !windows
// +build !windows
package terminal
import (
"fmt"
)
func EraseLine(out FileWriter, mode EraseLineMode) error {
_, err := fmt.Fprintf(out, "\x1b[%dK", mode)
return err
}

View File

@ -0,0 +1,31 @@
package terminal
import (
"syscall"
"unsafe"
)
func EraseLine(out FileWriter, mode EraseLineMode) error {
handle := syscall.Handle(out.Fd())
var csbi consoleScreenBufferInfo
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return err
}
var w uint32
var x Short
cursor := csbi.cursorPosition
switch mode {
case ERASE_LINE_END:
x = csbi.size.X
case ERASE_LINE_START:
x = 0
case ERASE_LINE_ALL:
cursor.X = 0
x = csbi.size.X
}
_, _, err := procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(x), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w)))
return normalizeError(err)
}

View File

@ -0,0 +1,10 @@
package terminal
import (
"errors"
)
var (
//lint:ignore ST1012 keeping old name for backwards compatibility
InterruptErr = errors.New("interrupt")
)

View File

@ -0,0 +1,20 @@
//go:build !windows
// +build !windows
package terminal
import (
"io"
)
// NewAnsiStdout returns special stdout, which converts escape sequences to Windows API calls
// on Windows environment.
func NewAnsiStdout(out FileWriter) io.Writer {
return out
}
// NewAnsiStderr returns special stderr, which converts escape sequences to Windows API calls
// on Windows environment.
func NewAnsiStderr(out FileWriter) io.Writer {
return out
}

View File

@ -0,0 +1,253 @@
package terminal
import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
"syscall"
"unsafe"
"github.com/mattn/go-isatty"
)
const (
foregroundBlue = 0x1
foregroundGreen = 0x2
foregroundRed = 0x4
foregroundIntensity = 0x8
foregroundMask = (foregroundRed | foregroundBlue | foregroundGreen | foregroundIntensity)
backgroundBlue = 0x10
backgroundGreen = 0x20
backgroundRed = 0x40
backgroundIntensity = 0x80
backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity)
)
type Writer struct {
out FileWriter
handle syscall.Handle
orgAttr word
}
func NewAnsiStdout(out FileWriter) io.Writer {
var csbi consoleScreenBufferInfo
if !isatty.IsTerminal(out.Fd()) {
return out
}
handle := syscall.Handle(out.Fd())
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
return &Writer{out: out, handle: handle, orgAttr: csbi.attributes}
}
func NewAnsiStderr(out FileWriter) io.Writer {
var csbi consoleScreenBufferInfo
if !isatty.IsTerminal(out.Fd()) {
return out
}
handle := syscall.Handle(out.Fd())
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
return &Writer{out: out, handle: handle, orgAttr: csbi.attributes}
}
func (w *Writer) Write(data []byte) (n int, err error) {
r := bytes.NewReader(data)
for {
var ch rune
var size int
ch, size, err = r.ReadRune()
if err != nil {
if err == io.EOF {
err = nil
}
return
}
n += size
switch ch {
case '\x1b':
size, err = w.handleEscape(r)
n += size
if err != nil {
return
}
default:
_, err = fmt.Fprint(w.out, string(ch))
if err != nil {
return
}
}
}
}
func (w *Writer) handleEscape(r *bytes.Reader) (n int, err error) {
buf := make([]byte, 0, 10)
buf = append(buf, "\x1b"...)
var ch rune
var size int
// Check '[' continues after \x1b
ch, size, err = r.ReadRune()
if err != nil {
if err == io.EOF {
err = nil
}
fmt.Fprint(w.out, string(buf))
return
}
n += size
if ch != '[' {
fmt.Fprint(w.out, string(buf))
return
}
// Parse escape code
var code rune
argBuf := make([]byte, 0, 10)
for {
ch, size, err = r.ReadRune()
if err != nil {
if err == io.EOF {
err = nil
}
fmt.Fprint(w.out, string(buf))
return
}
n += size
if ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') {
code = ch
break
}
argBuf = append(argBuf, string(ch)...)
}
err = w.applyEscapeCode(buf, string(argBuf), code)
return
}
func (w *Writer) applyEscapeCode(buf []byte, arg string, code rune) error {
c := &Cursor{Out: w.out}
switch arg + string(code) {
case "?25h":
return c.Show()
case "?25l":
return c.Hide()
}
if code >= 'A' && code <= 'G' {
if n, err := strconv.Atoi(arg); err == nil {
switch code {
case 'A':
return c.Up(n)
case 'B':
return c.Down(n)
case 'C':
return c.Forward(n)
case 'D':
return c.Back(n)
case 'E':
return c.NextLine(n)
case 'F':
return c.PreviousLine(n)
case 'G':
return c.HorizontalAbsolute(n)
}
}
}
switch code {
case 'm':
return w.applySelectGraphicRendition(arg)
default:
buf = append(buf, string(code)...)
_, err := fmt.Fprint(w.out, string(buf))
return err
}
}
// Original implementation: https://github.com/mattn/go-colorable
func (w *Writer) applySelectGraphicRendition(arg string) error {
if arg == "" {
_, _, err := procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.orgAttr))
return normalizeError(err)
}
var csbi consoleScreenBufferInfo
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
return err
}
attr := csbi.attributes
for _, param := range strings.Split(arg, ";") {
n, err := strconv.Atoi(param)
if err != nil {
continue
}
switch {
case n == 0 || n == 100:
attr = w.orgAttr
case 1 <= n && n <= 5:
attr |= foregroundIntensity
case 30 <= n && n <= 37:
attr = (attr & backgroundMask)
if (n-30)&1 != 0 {
attr |= foregroundRed
}
if (n-30)&2 != 0 {
attr |= foregroundGreen
}
if (n-30)&4 != 0 {
attr |= foregroundBlue
}
case 40 <= n && n <= 47:
attr = (attr & foregroundMask)
if (n-40)&1 != 0 {
attr |= backgroundRed
}
if (n-40)&2 != 0 {
attr |= backgroundGreen
}
if (n-40)&4 != 0 {
attr |= backgroundBlue
}
case 90 <= n && n <= 97:
attr = (attr & backgroundMask)
attr |= foregroundIntensity
if (n-90)&1 != 0 {
attr |= foregroundRed
}
if (n-90)&2 != 0 {
attr |= foregroundGreen
}
if (n-90)&4 != 0 {
attr |= foregroundBlue
}
case 100 <= n && n <= 107:
attr = (attr & foregroundMask)
attr |= backgroundIntensity
if (n-100)&1 != 0 {
attr |= backgroundRed
}
if (n-100)&2 != 0 {
attr |= backgroundGreen
}
if (n-100)&4 != 0 {
attr |= backgroundBlue
}
}
}
_, _, err := procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr))
return normalizeError(err)
}
func normalizeError(err error) error {
if syserr, ok := err.(syscall.Errno); ok && syserr == 0 {
return nil
}
return err
}

View File

@ -0,0 +1,417 @@
package terminal
import (
"fmt"
"unicode"
"golang.org/x/text/width"
)
type RuneReader struct {
stdio Stdio
state runeReaderState
}
func NewRuneReader(stdio Stdio) *RuneReader {
return &RuneReader{
stdio: stdio,
state: newRuneReaderState(stdio.In),
}
}
func (rr *RuneReader) printChar(char rune, mask rune) error {
// if we don't need to mask the input
if mask == 0 {
// just print the character the user pressed
_, err := fmt.Fprintf(rr.stdio.Out, "%c", char)
return err
}
// otherwise print the mask we were given
_, err := fmt.Fprintf(rr.stdio.Out, "%c", mask)
return err
}
type OnRuneFn func(rune, []rune) ([]rune, bool, error)
func (rr *RuneReader) ReadLine(mask rune, onRunes ...OnRuneFn) ([]rune, error) {
return rr.ReadLineWithDefault(mask, []rune{}, onRunes...)
}
func (rr *RuneReader) ReadLineWithDefault(mask rune, d []rune, onRunes ...OnRuneFn) ([]rune, error) {
line := []rune{}
// we only care about horizontal displacements from the origin so start counting at 0
index := 0
cursor := &Cursor{
In: rr.stdio.In,
Out: rr.stdio.Out,
}
onRune := func(r rune, line []rune) ([]rune, bool, error) {
return line, false, nil
}
// if the user pressed a key the caller was interested in capturing
if len(onRunes) > 0 {
onRune = onRunes[0]
}
// we get the terminal width and height (if resized after this point the property might become invalid)
terminalSize, _ := cursor.Size(rr.Buffer())
// we set the current location of the cursor once
cursorCurrent, _ := cursor.Location(rr.Buffer())
increment := func() {
if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
cursorCurrent.X = COORDINATE_SYSTEM_BEGIN
cursorCurrent.Y++
} else {
cursorCurrent.X++
}
}
decrement := func() {
if cursorCurrent.CursorIsAtLineBegin() {
cursorCurrent.X = terminalSize.X
cursorCurrent.Y--
} else {
cursorCurrent.X--
}
}
if len(d) > 0 {
index = len(d)
if _, err := fmt.Fprint(rr.stdio.Out, string(d)); err != nil {
return d, err
}
line = d
for range d {
increment()
}
}
for {
// wait for some input
r, _, err := rr.ReadRune()
if err != nil {
return line, err
}
if l, stop, err := onRune(r, line); stop || err != nil {
return l, err
}
// if the user pressed enter or some other newline/termination like ctrl+d
if r == '\r' || r == '\n' || r == KeyEndTransmission {
// delete what's printed out on the console screen (cleanup)
for index > 0 {
if cursorCurrent.CursorIsAtLineBegin() {
EraseLine(rr.stdio.Out, ERASE_LINE_END)
cursor.PreviousLine(1)
cursor.Forward(int(terminalSize.X))
} else {
cursor.Back(1)
}
decrement()
index--
}
// move the cursor the a new line
cursor.MoveNextLine(cursorCurrent, terminalSize)
// we're done processing the input
return line, nil
}
// if the user interrupts (ie with ctrl+c)
if r == KeyInterrupt {
// go to the beginning of the next line
if _, err := fmt.Fprint(rr.stdio.Out, "\r\n"); err != nil {
return line, err
}
// we're done processing the input, and treat interrupt like an error
return line, InterruptErr
}
// allow for backspace/delete editing of inputs
if r == KeyBackspace || r == KeyDelete {
// and we're not at the beginning of the line
if index > 0 && len(line) > 0 {
// if we are at the end of the word
if index == len(line) {
// just remove the last letter from the internal representation
// also count the number of cells the rune before the cursor occupied
cells := runeWidth(line[len(line)-1])
line = line[:len(line)-1]
// go back one
if cursorCurrent.X == 1 {
cursor.PreviousLine(1)
cursor.Forward(int(terminalSize.X))
} else {
cursor.Back(cells)
}
// clear the rest of the line
EraseLine(rr.stdio.Out, ERASE_LINE_END)
} else {
// we need to remove a character from the middle of the word
cells := runeWidth(line[index-1])
// remove the current index from the list
line = append(line[:index-1], line[index:]...)
// save the current position of the cursor, as we have to move the cursor one back to erase the current symbol
// and then move the cursor for each symbol in line[index-1:] to print it out, afterwards we want to restore
// the cursor to its previous location.
cursor.Save()
// clear the rest of the line
cursor.Back(cells)
// print what comes after
for _, char := range line[index-1:] {
//Erase symbols which are left over from older print
EraseLine(rr.stdio.Out, ERASE_LINE_END)
// print characters to the new line appropriately
if err := rr.printChar(char, mask); err != nil {
return line, err
}
}
// erase what's left over from last print
if cursorCurrent.Y < terminalSize.Y {
cursor.NextLine(1)
EraseLine(rr.stdio.Out, ERASE_LINE_END)
}
// restore cursor
cursor.Restore()
if cursorCurrent.CursorIsAtLineBegin() {
cursor.PreviousLine(1)
cursor.Forward(int(terminalSize.X))
} else {
cursor.Back(cells)
}
}
// decrement the index
index--
decrement()
} else {
// otherwise the user pressed backspace while at the beginning of the line
_ = soundBell(rr.stdio.Out)
}
// we're done processing this key
continue
}
// if the left arrow is pressed
if r == KeyArrowLeft {
// if we have space to the left
if index > 0 {
//move the cursor to the prev line if necessary
if cursorCurrent.CursorIsAtLineBegin() {
cursor.PreviousLine(1)
cursor.Forward(int(terminalSize.X))
} else {
cursor.Back(runeWidth(line[index-1]))
}
//decrement the index
index--
decrement()
} else {
// otherwise we are at the beginning of where we started reading lines
// sound the bell
_ = soundBell(rr.stdio.Out)
}
// we're done processing this key press
continue
}
// if the right arrow is pressed
if r == KeyArrowRight {
// if we have space to the right
if index < len(line) {
// move the cursor to the next line if necessary
if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
cursor.NextLine(1)
} else {
cursor.Forward(runeWidth(line[index]))
}
index++
increment()
} else {
// otherwise we are at the end of the word and can't go past
// sound the bell
_ = soundBell(rr.stdio.Out)
}
// we're done processing this key press
continue
}
// the user pressed one of the special keys
if r == SpecialKeyHome {
for index > 0 {
if cursorCurrent.CursorIsAtLineBegin() {
cursor.PreviousLine(1)
cursor.Forward(int(terminalSize.X))
cursorCurrent.Y--
cursorCurrent.X = terminalSize.X
} else {
cursor.Back(runeWidth(line[index-1]))
cursorCurrent.X -= Short(runeWidth(line[index-1]))
}
index--
}
continue
// user pressed end
} else if r == SpecialKeyEnd {
for index != len(line) {
if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
cursor.NextLine(1)
cursorCurrent.Y++
cursorCurrent.X = COORDINATE_SYSTEM_BEGIN
} else {
cursor.Forward(runeWidth(line[index]))
cursorCurrent.X += Short(runeWidth(line[index]))
}
index++
}
continue
// user pressed forward delete key
} else if r == SpecialKeyDelete {
// if index at the end of the line nothing to delete
if index != len(line) {
// save the current position of the cursor, as we have to erase the current symbol
// and then move the cursor for each symbol in line[index:] to print it out, afterwards we want to restore
// the cursor to its previous location.
cursor.Save()
// remove the symbol after the cursor
line = append(line[:index], line[index+1:]...)
// print the updated line
for _, char := range line[index:] {
EraseLine(rr.stdio.Out, ERASE_LINE_END)
// print out the character
if err := rr.printChar(char, mask); err != nil {
return line, err
}
}
// erase what's left on last line
if cursorCurrent.Y < terminalSize.Y {
cursor.NextLine(1)
EraseLine(rr.stdio.Out, ERASE_LINE_END)
}
// restore cursor
cursor.Restore()
if len(line) == 0 || index == len(line) {
EraseLine(rr.stdio.Out, ERASE_LINE_END)
}
}
continue
}
// if the letter is another escape sequence
if unicode.IsControl(r) || r == IgnoreKey {
// ignore it
continue
}
// the user pressed a regular key
// if we are at the end of the line
if index == len(line) {
// just append the character at the end of the line
line = append(line, r)
// save the location of the cursor
index++
increment()
// print out the character
if err := rr.printChar(r, mask); err != nil {
return line, err
}
} else {
// we are in the middle of the word so we need to insert the character the user pressed
line = append(line[:index], append([]rune{r}, line[index:]...)...)
// save the current position of the cursor, as we have to move the cursor back to erase the current symbol
// and then move for each symbol in line[index:] to print it out, afterwards we want to restore
// cursor's location to its previous one.
cursor.Save()
EraseLine(rr.stdio.Out, ERASE_LINE_END)
// remove the symbol after the cursor
// print the updated line
for _, char := range line[index:] {
EraseLine(rr.stdio.Out, ERASE_LINE_END)
// print out the character
if err := rr.printChar(char, mask); err != nil {
return line, err
}
increment()
}
// if we are at the last line, we want to visually insert a new line and append to it.
if cursorCurrent.CursorIsAtLineEnd(terminalSize) && cursorCurrent.Y == terminalSize.Y {
// add a new line to the terminal
if _, err := fmt.Fprintln(rr.stdio.Out); err != nil {
return line, err
}
// restore the position of the cursor horizontally
cursor.Restore()
// restore the position of the cursor vertically
cursor.PreviousLine(1)
} else {
// restore cursor
cursor.Restore()
}
// check if cursor needs to move to next line
cursorCurrent, _ = cursor.Location(rr.Buffer())
if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
cursor.NextLine(1)
} else {
cursor.Forward(runeWidth(r))
}
// increment the index
index++
increment()
}
}
}
// runeWidth returns the number of columns spanned by a rune when printed to the terminal
func runeWidth(r rune) int {
switch width.LookupRune(r).Kind() {
case width.EastAsianWide, width.EastAsianFullwidth:
return 2
}
if !unicode.IsPrint(r) {
return 0
}
return 1
}
// isAnsiMarker returns if a rune denotes the start of an ANSI sequence
func isAnsiMarker(r rune) bool {
return r == '\x1B'
}
// isAnsiTerminator returns if a rune denotes the end of an ANSI sequence
func isAnsiTerminator(r rune) bool {
return (r >= 0x40 && r <= 0x5a) || (r == 0x5e) || (r >= 0x60 && r <= 0x7e)
}
// StringWidth returns the visible width of a string when printed to the terminal
func StringWidth(str string) int {
w := 0
ansi := false
for _, r := range str {
// increase width only when outside of ANSI escape sequences
if ansi || isAnsiMarker(r) {
ansi = !isAnsiTerminator(r)
} else {
w += runeWidth(r)
}
}
return w
}

View File

@ -0,0 +1,14 @@
// copied from: https://github.com/golang/crypto/blob/master/ssh/terminal/util_bsd.go
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build darwin || dragonfly || freebsd || netbsd || openbsd
// +build darwin dragonfly freebsd netbsd openbsd
package terminal
import "syscall"
const ioctlReadTermios = syscall.TIOCGETA
const ioctlWriteTermios = syscall.TIOCSETA

View File

@ -0,0 +1,14 @@
// copied from https://github.com/golang/crypto/blob/master/ssh/terminal/util_linux.go
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && !ppc64le
// +build linux,!ppc64le
package terminal
// These constants are declared here, rather than importing
// them from the syscall package as some syscall packages, even
// on linux, for example gccgo, do not declare them.
const ioctlReadTermios = 0x5401 // syscall.TCGETS
const ioctlWriteTermios = 0x5402 // syscall.TCSETS

View File

@ -0,0 +1,132 @@
//go:build !windows
// +build !windows
// The terminal mode manipulation code is derived heavily from:
// https://github.com/golang/crypto/blob/master/ssh/terminal/util.go:
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package terminal
import (
"bufio"
"bytes"
"fmt"
"syscall"
"unsafe"
)
const (
normalKeypad = '['
applicationKeypad = 'O'
)
type runeReaderState struct {
term syscall.Termios
reader *bufio.Reader
buf *bytes.Buffer
}
func newRuneReaderState(input FileReader) runeReaderState {
buf := new(bytes.Buffer)
return runeReaderState{
reader: bufio.NewReader(&BufferedReader{
In: input,
Buffer: buf,
}),
buf: buf,
}
}
func (rr *RuneReader) Buffer() *bytes.Buffer {
return rr.state.buf
}
// For reading runes we just want to disable echo.
func (rr *RuneReader) SetTermMode() error {
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(rr.stdio.In.Fd()), ioctlReadTermios, uintptr(unsafe.Pointer(&rr.state.term)), 0, 0, 0); err != 0 {
return err
}
newState := rr.state.term
newState.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG
// Because we are clearing canonical mode, we need to ensure VMIN & VTIME are
// set to the values we expect. This combination puts things in standard
// "blocking read" mode (see termios(3)).
newState.Cc[syscall.VMIN] = 1
newState.Cc[syscall.VTIME] = 0
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(rr.stdio.In.Fd()), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 {
return err
}
return nil
}
func (rr *RuneReader) RestoreTermMode() error {
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(rr.stdio.In.Fd()), ioctlWriteTermios, uintptr(unsafe.Pointer(&rr.state.term)), 0, 0, 0); err != 0 {
return err
}
return nil
}
// ReadRune Parse escape sequences such as ESC [ A for arrow keys.
// See https://vt100.net/docs/vt102-ug/appendixc.html
func (rr *RuneReader) ReadRune() (rune, int, error) {
r, size, err := rr.state.reader.ReadRune()
if err != nil {
return r, size, err
}
if r != KeyEscape {
return r, size, err
}
if rr.state.reader.Buffered() == 0 {
// no more characters so must be `Esc` key
return KeyEscape, 1, nil
}
r, size, err = rr.state.reader.ReadRune()
if err != nil {
return r, size, err
}
// ESC O ... or ESC [ ...?
if r != normalKeypad && r != applicationKeypad {
return r, size, fmt.Errorf("unexpected escape sequence from terminal: %q", []rune{KeyEscape, r})
}
keypad := r
r, size, err = rr.state.reader.ReadRune()
if err != nil {
return r, size, err
}
switch r {
case 'A': // ESC [ A or ESC O A
return KeyArrowUp, 1, nil
case 'B': // ESC [ B or ESC O B
return KeyArrowDown, 1, nil
case 'C': // ESC [ C or ESC O C
return KeyArrowRight, 1, nil
case 'D': // ESC [ D or ESC O D
return KeyArrowLeft, 1, nil
case 'F': // ESC [ F or ESC O F
return SpecialKeyEnd, 1, nil
case 'H': // ESC [ H or ESC O H
return SpecialKeyHome, 1, nil
case '3': // ESC [ 3
if keypad == normalKeypad {
// discard the following '~' key from buffer
_, _ = rr.state.reader.Discard(1)
return SpecialKeyDelete, 1, nil
}
}
// discard the following '~' key from buffer
_, _ = rr.state.reader.Discard(1)
return IgnoreKey, 1, nil
}

View File

@ -0,0 +1,8 @@
//go:build ppc64le && linux
// +build ppc64le,linux
package terminal
// Used syscall numbers from https://github.com/golang/go/blob/master/src/syscall/ztypes_linux_ppc64le.go
const ioctlReadTermios = 0x402c7413 // syscall.TCGETS
const ioctlWriteTermios = 0x802c7414 // syscall.TCSETS

View File

@ -0,0 +1,142 @@
package terminal
import (
"bytes"
"syscall"
"unsafe"
)
var (
dll = syscall.NewLazyDLL("kernel32.dll")
setConsoleMode = dll.NewProc("SetConsoleMode")
getConsoleMode = dll.NewProc("GetConsoleMode")
readConsoleInput = dll.NewProc("ReadConsoleInputW")
)
const (
EVENT_KEY = 0x0001
// key codes for arrow keys
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
VK_DELETE = 0x2E
VK_END = 0x23
VK_HOME = 0x24
VK_LEFT = 0x25
VK_UP = 0x26
VK_RIGHT = 0x27
VK_DOWN = 0x28
RIGHT_CTRL_PRESSED = 0x0004
LEFT_CTRL_PRESSED = 0x0008
ENABLE_ECHO_INPUT uint32 = 0x0004
ENABLE_LINE_INPUT uint32 = 0x0002
ENABLE_PROCESSED_INPUT uint32 = 0x0001
)
type inputRecord struct {
eventType uint16
padding uint16
event [16]byte
}
type keyEventRecord struct {
bKeyDown int32
wRepeatCount uint16
wVirtualKeyCode uint16
wVirtualScanCode uint16
unicodeChar uint16
wdControlKeyState uint32
}
type runeReaderState struct {
term uint32
}
func newRuneReaderState(input FileReader) runeReaderState {
return runeReaderState{}
}
func (rr *RuneReader) Buffer() *bytes.Buffer {
return nil
}
func (rr *RuneReader) SetTermMode() error {
r, _, err := getConsoleMode.Call(uintptr(rr.stdio.In.Fd()), uintptr(unsafe.Pointer(&rr.state.term)))
// windows return 0 on error
if r == 0 {
return err
}
newState := rr.state.term
newState &^= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT
r, _, err = setConsoleMode.Call(uintptr(rr.stdio.In.Fd()), uintptr(newState))
// windows return 0 on error
if r == 0 {
return err
}
return nil
}
func (rr *RuneReader) RestoreTermMode() error {
r, _, err := setConsoleMode.Call(uintptr(rr.stdio.In.Fd()), uintptr(rr.state.term))
// windows return 0 on error
if r == 0 {
return err
}
return nil
}
func (rr *RuneReader) ReadRune() (rune, int, error) {
ir := &inputRecord{}
bytesRead := 0
for {
rv, _, e := readConsoleInput.Call(rr.stdio.In.Fd(), uintptr(unsafe.Pointer(ir)), 1, uintptr(unsafe.Pointer(&bytesRead)))
// windows returns non-zero to indicate success
if rv == 0 && e != nil {
return 0, 0, e
}
if ir.eventType != EVENT_KEY {
continue
}
// the event data is really a c struct union, so here we have to do an usafe
// cast to put the data into the keyEventRecord (since we have already verified
// above that this event does correspond to a key event
key := (*keyEventRecord)(unsafe.Pointer(&ir.event[0]))
// we only care about key down events
if key.bKeyDown == 0 {
continue
}
if key.wdControlKeyState&(LEFT_CTRL_PRESSED|RIGHT_CTRL_PRESSED) != 0 && key.unicodeChar == 'C' {
return KeyInterrupt, bytesRead, nil
}
// not a normal character so look up the input sequence from the
// virtual key code mappings (VK_*)
if key.unicodeChar == 0 {
switch key.wVirtualKeyCode {
case VK_DOWN:
return KeyArrowDown, bytesRead, nil
case VK_LEFT:
return KeyArrowLeft, bytesRead, nil
case VK_RIGHT:
return KeyArrowRight, bytesRead, nil
case VK_UP:
return KeyArrowUp, bytesRead, nil
case VK_DELETE:
return SpecialKeyDelete, bytesRead, nil
case VK_HOME:
return SpecialKeyHome, bytesRead, nil
case VK_END:
return SpecialKeyEnd, bytesRead, nil
default:
// not a virtual key that we care about so just continue on to
// the next input key
continue
}
}
r := rune(key.unicodeChar)
return r, bytesRead, nil
}
}

View File

@ -0,0 +1,32 @@
package terminal
import (
"fmt"
"io"
)
const (
KeyArrowLeft = '\x02'
KeyArrowRight = '\x06'
KeyArrowUp = '\x10'
KeyArrowDown = '\x0e'
KeySpace = ' '
KeyEnter = '\r'
KeyBackspace = '\b'
KeyDelete = '\x7f'
KeyInterrupt = '\x03'
KeyEndTransmission = '\x04'
KeyEscape = '\x1b'
KeyDeleteWord = '\x17' // Ctrl+W
KeyDeleteLine = '\x18' // Ctrl+X
SpecialKeyHome = '\x01'
SpecialKeyEnd = '\x11'
SpecialKeyDelete = '\x12'
IgnoreKey = '\000'
KeyTab = '\t'
)
func soundBell(out io.Writer) error {
_, err := fmt.Fprint(out, "\a")
return err
}

View File

@ -0,0 +1,24 @@
package terminal
import (
"io"
)
// Stdio is the standard input/output the terminal reads/writes with.
type Stdio struct {
In FileReader
Out FileWriter
Err io.Writer
}
// FileWriter provides a minimal interface for Stdin.
type FileWriter interface {
io.Writer
Fd() uintptr
}
// FileReader provides a minimal interface for Stdout.
type FileReader interface {
io.Reader
Fd() uintptr
}

View File

@ -0,0 +1,39 @@
package terminal
import (
"syscall"
)
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
procSetConsoleTextAttribute = kernel32.NewProc("SetConsoleTextAttribute")
procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW")
procGetConsoleCursorInfo = kernel32.NewProc("GetConsoleCursorInfo")
procSetConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo")
)
type wchar uint16
type dword uint32
type word uint16
type smallRect struct {
left Short
top Short
right Short
bottom Short
}
type consoleScreenBufferInfo struct {
size Coord
cursorPosition Coord
attributes word
window smallRect
maximumWindowSize Coord
}
type consoleCursorInfo struct {
size dword
visible int32
}

View File

@ -0,0 +1,8 @@
package terminal
type Short int16
type Coord struct {
X Short
Y Short
}