Use Survey For Interactions With User (#186)

fixes

Use Survey For Interactions With User

Add Vendor "github.com/AlecAivazis/survey/v2"

Co-authored-by: Norwin Roosen <git@nroo.de>
Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://gitea.com/gitea/tea/pulls/186
Reviewed-by: techknowlogick <techknowlogick@gitea.io>
Reviewed-by: Norwin <noerw@noreply.gitea.io>
This commit is contained in:
6543
2020-10-03 02:54:09 +00:00
parent e23f56e81c
commit 7ac3ffcc1b
81 changed files with 12088 additions and 40 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,190 @@
// +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) {
fmt.Fprintf(c.Out, "\x1b[%dA", n)
}
// Down moves the cursor n cells to down.
func (c *Cursor) Down(n int) {
fmt.Fprintf(c.Out, "\x1b[%dB", n)
}
// Forward moves the cursor n cells to right.
func (c *Cursor) Forward(n int) {
fmt.Fprintf(c.Out, "\x1b[%dC", n)
}
// Back moves the cursor n cells to left.
func (c *Cursor) Back(n int) {
fmt.Fprintf(c.Out, "\x1b[%dD", n)
}
// NextLine moves cursor to beginning of the line n lines down.
func (c *Cursor) NextLine(n int) {
fmt.Fprintf(c.Out, "\x1b[%dE", n)
}
// PreviousLine moves cursor to beginning of the line n lines up.
func (c *Cursor) PreviousLine(n int) {
fmt.Fprintf(c.Out, "\x1b[%dF", n)
}
// HorizontalAbsolute moves cursor horizontally to x.
func (c *Cursor) HorizontalAbsolute(x int) {
fmt.Fprintf(c.Out, "\x1b[%dG", x)
}
// Show shows the cursor.
func (c *Cursor) Show() {
fmt.Fprint(c.Out, "\x1b[?25h")
}
// Hide hide the cursor.
func (c *Cursor) Hide() {
fmt.Fprint(c.Out, "\x1b[?25l")
}
// Move moves the cursor to a specific x,y location.
func (c *Cursor) Move(x int, y int) {
fmt.Fprintf(c.Out, "\x1b[%d;%df", x, y)
}
// Save saves the current position
func (c *Cursor) Save() {
fmt.Fprint(c.Out, "\x1b7")
}
// Restore restores the saved position of the cursor
func (c *Cursor) Restore() {
fmt.Fprint(c.Out, "\x1b8")
}
// 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) {
if cur.Y == terminalSize.Y {
fmt.Fprintln(c.Out)
}
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
fmt.Fprint(c.Out, "\x1b[6n")
// 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()
// save the current location of the cursor
c.Save()
// 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
}
// move back where we began
c.Restore()
// show the cursor
c.Show()
// 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,138 @@
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) {
c.cursorMove(0, n)
}
func (c *Cursor) Down(n int) {
c.cursorMove(0, -1*n)
}
func (c *Cursor) Forward(n int) {
c.cursorMove(n, 0)
}
func (c *Cursor) Back(n int) {
c.cursorMove(-1*n, 0)
}
// save the cursor location
func (c *Cursor) Save() {
cursorLoc, _ = c.Location(nil)
}
func (c *Cursor) Restore() {
handle := syscall.Handle(c.Out.Fd())
// restore it to the original position
procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursorLoc))))
}
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) {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
var cursor Coord
cursor.X = csbi.cursorPosition.X + Short(x)
cursor.Y = csbi.cursorPosition.Y + Short(y)
procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
}
func (c *Cursor) NextLine(n int) {
c.Up(n)
c.HorizontalAbsolute(0)
}
func (c *Cursor) PreviousLine(n int) {
c.Down(n)
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) {
c.NextLine(1)
}
func (c *Cursor) HorizontalAbsolute(x int) {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
var cursor Coord
cursor.X = Short(x)
cursor.Y = csbi.cursorPosition.Y
if csbi.size.X < cursor.X {
cursor.X = csbi.size.X
}
procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
}
func (c *Cursor) Show() {
handle := syscall.Handle(c.Out.Fd())
var cci consoleCursorInfo
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
cci.visible = 1
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
}
func (c *Cursor) Hide() {
handle := syscall.Handle(c.Out.Fd())
var cci consoleCursorInfo
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
cci.visible = 0
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
}
func (c *Cursor) Location(buf *bytes.Buffer) (Coord, error) {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
return csbi.cursorPosition, nil
}
func (c *Cursor) Size(buf *bytes.Buffer) (*Coord, error) {
handle := syscall.Handle(c.Out.Fd())
var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
// 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,11 @@
// +build !windows
package terminal
import (
"fmt"
)
func EraseLine(out FileWriter, mode EraseLineMode) {
fmt.Fprintf(out, "\x1b[%dK", mode)
}

View File

@ -0,0 +1,27 @@
package terminal
import (
"syscall"
"unsafe"
)
func EraseLine(out FileWriter, mode EraseLineMode) {
handle := syscall.Handle(out.Fd())
var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
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
}
procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(x), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w)))
}

View File

@ -0,0 +1,9 @@
package terminal
import (
"errors"
)
var (
InterruptErr = errors.New("interrupt")
)

View File

@ -0,0 +1,19 @@
// +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,227 @@
package terminal
import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
"syscall"
"unsafe"
"github.com/mattn/go-isatty"
)
var (
cursorFunctions = map[rune]func(c *Cursor) func(int){
'A': func(c *Cursor) func(int) { return c.Up },
'B': func(c *Cursor) func(int) { return c.Down },
'C': func(c *Cursor) func(int) { return c.Forward },
'D': func(c *Cursor) func(int) { return c.Back },
'E': func(c *Cursor) func(int) { return c.NextLine },
'F': func(c *Cursor) func(int) { return c.PreviousLine },
'G': func(c *Cursor) func(int) { return c.HorizontalAbsolute },
}
)
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 {
ch, size, err := r.ReadRune()
if err != nil {
break
}
n += size
switch ch {
case '\x1b':
size, err = w.handleEscape(r)
n += size
if err != nil {
break
}
default:
fmt.Fprint(w.out, string(ch))
}
}
return
}
func (w *Writer) handleEscape(r *bytes.Reader) (n int, err error) {
buf := make([]byte, 0, 10)
buf = append(buf, "\x1b"...)
// Check '[' continues after \x1b
ch, size, err := r.ReadRune()
if 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 {
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)...)
}
w.applyEscapeCode(buf, string(argBuf), code)
return
}
func (w *Writer) applyEscapeCode(buf []byte, arg string, code rune) {
c := &Cursor{Out: w.out}
switch arg + string(code) {
case "?25h":
c.Show()
return
case "?25l":
c.Hide()
return
}
if f, ok := cursorFunctions[code]; ok {
if n, err := strconv.Atoi(arg); err == nil {
f(c)(n)
return
}
}
switch code {
case 'm':
w.applySelectGraphicRendition(arg)
default:
buf = append(buf, string(code)...)
fmt.Fprint(w.out, string(buf))
}
}
// Original implementation: https://github.com/mattn/go-colorable
func (w *Writer) applySelectGraphicRendition(arg string) {
if arg == "" {
procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.orgAttr))
return
}
var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
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
}
}
}
procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr))
}

View File

@ -0,0 +1,330 @@
package terminal
import (
"fmt"
"unicode"
"golang.org/x/text/width"
)
type RuneReader struct {
stdio Stdio
cursor *Cursor
state runeReaderState
}
func NewRuneReader(stdio Stdio) *RuneReader {
return &RuneReader{
stdio: stdio,
state: newRuneReaderState(stdio.In),
}
}
func (rr *RuneReader) printChar(char rune, mask rune) {
// if we don't need to mask the input
if mask == 0 {
// just print the character the user pressed
fmt.Fprintf(rr.stdio.Out, "%c", char)
} else {
// otherwise print the mask we were given
fmt.Fprintf(rr.stdio.Out, "%c", mask)
}
}
func (rr *RuneReader) ReadLine(mask rune) ([]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,
}
// 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())
for {
// wait for some input
r, _, err := rr.ReadRune()
if err != nil {
return line, err
}
// increment cursor location
cursorCurrent.X++
// 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))
cursorCurrent.X = terminalSize.X
cursorCurrent.Y--
} else {
cursor.Back(1)
cursorCurrent.X--
}
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
fmt.Fprint(rr.stdio.Out, "\r\n")
// 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
rr.printChar(char, mask)
}
// 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--
} 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--
} 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++
} 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.X = terminalSize.X
cursorCurrent.Y--
} else {
cursor.Back(runeWidth(line[index-1]))
cursorCurrent.X--
}
index--
}
continue
// user pressed end
} else if r == SpecialKeyEnd {
for index != len(line) {
if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
cursor.NextLine(1)
cursorCurrent.X = COORDINATE_SYSTEM_BEGIN
cursorCurrent.Y++
} else {
cursor.Forward(runeWidth(line[index]))
cursorCurrent.X++
}
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
rr.printChar(char, mask)
}
// 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++
// print out the character
rr.printChar(r, mask)
} 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
rr.printChar(char, mask)
cursorCurrent.X++
}
// 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
fmt.Fprintln(rr.stdio.Out)
// restore the position of the cursor horizontally
cursor.Restore()
// restore the position of the cursor vertically
cursor.Up(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++
}
}
}
func runeWidth(r rune) int {
switch width.LookupRune(r).Kind() {
case width.EastAsianWide, width.EastAsianFullwidth:
return 2
}
return 1
}

View File

@ -0,0 +1,13 @@
// 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.
// +build darwin dragonfly freebsd netbsd openbsd
package terminal
import "syscall"
const ioctlReadTermios = syscall.TIOCGETA
const ioctlWriteTermios = syscall.TIOCSETA

View File

@ -0,0 +1,13 @@
// 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.
// +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,111 @@
// +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"
)
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
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
}
func (rr *RuneReader) ReadRune() (rune, int, error) {
r, size, err := rr.state.reader.ReadRune()
if err != nil {
return r, size, err
}
// parse ^[ sequences to look for arrow keys
if r == '\033' {
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
}
if r != '[' {
return r, size, fmt.Errorf("Unexpected Escape Sequence: %q", []rune{'\033', r})
}
r, size, err = rr.state.reader.ReadRune()
if err != nil {
return r, size, err
}
switch r {
case 'D':
return KeyArrowLeft, 1, nil
case 'C':
return KeyArrowRight, 1, nil
case 'A':
return KeyArrowUp, 1, nil
case 'B':
return KeyArrowDown, 1, nil
case 'H': // Home button
return SpecialKeyHome, 1, nil
case 'F': // End button
return SpecialKeyEnd, 1, nil
case '3': // Delete Button
// discard the following '~' key from buffer
rr.state.reader.Discard(1)
return SpecialKeyDelete, 1, nil
default:
// discard the following '~' key from buffer
rr.state.reader.Discard(1)
return IgnoreKey, 1, nil
}
return r, size, fmt.Errorf("Unknown Escape Sequence: %q", []rune{'\033', '[', r})
}
return r, size, err
}

View File

@ -0,0 +1,7 @@
// +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,30 @@
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'
)
func soundBell(out io.Writer) {
fmt.Fprint(out, "\a")
}

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
}