|
|
|
@ -1,4 +1,33 @@
|
|
|
|
|
package main
|
|
|
|
|
package qline
|
|
|
|
|
|
|
|
|
|
// qline is a line input library for terminals that utilize
|
|
|
|
|
// vt100 compatible escape sequences
|
|
|
|
|
//
|
|
|
|
|
// Copyright © 2021 Brian Evans
|
|
|
|
|
//
|
|
|
|
|
// 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.
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bufio"
|
|
|
|
@ -17,7 +46,6 @@ const (
|
|
|
|
|
Delete
|
|
|
|
|
Home
|
|
|
|
|
End
|
|
|
|
|
PrintScreen
|
|
|
|
|
PageUp
|
|
|
|
|
PageDown
|
|
|
|
|
Escape rune = 27
|
|
|
|
@ -30,7 +58,7 @@ var (
|
|
|
|
|
width int
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Buffer struct {
|
|
|
|
|
type buffer struct {
|
|
|
|
|
buf []rune
|
|
|
|
|
cursor int
|
|
|
|
|
maxWidth int
|
|
|
|
@ -39,11 +67,56 @@ type Buffer struct {
|
|
|
|
|
prompt string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (lb Buffer) String() string {
|
|
|
|
|
func GetInput(prompt string, content string) string {
|
|
|
|
|
termios.SetCharMode()
|
|
|
|
|
cols, _ := termios.GetWindowSize()
|
|
|
|
|
b := buffer{make([]rune, 0, (len(content)+1)*2), 0, cols-len(prompt), 0, 0, prompt}
|
|
|
|
|
seeded := false
|
|
|
|
|
|
|
|
|
|
fmt.Print(prompt)
|
|
|
|
|
fmt.Print("\033[6n")
|
|
|
|
|
|
|
|
|
|
var ch rune
|
|
|
|
|
var err error
|
|
|
|
|
reader := bufio.NewReader(os.Stdin)
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
ch, err = readKey(reader)
|
|
|
|
|
if err != nil && err.Error() == "response" {
|
|
|
|
|
b.cursorStart = int(ch)
|
|
|
|
|
b.maxWidth = cols - b.cursorStart
|
|
|
|
|
} else if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if ch == CarriageReturn || ch == NewLine {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if isControl(ch) {
|
|
|
|
|
b.controlInput(ch)
|
|
|
|
|
} else {
|
|
|
|
|
b.addChar(ch, true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Seeding the content needs to happen
|
|
|
|
|
// _after_ reading in the width from the [6n
|
|
|
|
|
// function call.
|
|
|
|
|
if !seeded {
|
|
|
|
|
b.seedContent(content)
|
|
|
|
|
b.printBuf()
|
|
|
|
|
seeded = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
return b.string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (lb buffer) string() string {
|
|
|
|
|
return string(lb.buf)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (lb *Buffer) deleteChar() {
|
|
|
|
|
func (lb *buffer) deleteChar() {
|
|
|
|
|
if lb.cursor == len(lb.buf) {
|
|
|
|
|
return
|
|
|
|
|
} else if lb.cursor == len(lb.buf)-1 {
|
|
|
|
@ -56,10 +129,7 @@ func (lb *Buffer) deleteChar() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO rework this so that the changes are made but all drawing to the screen comes
|
|
|
|
|
// from one source and always draws the whole input area. This will make dealing with
|
|
|
|
|
// offsets much easier.
|
|
|
|
|
func (lb *Buffer) addChar(c rune, echo bool) {
|
|
|
|
|
func (lb *buffer) addChar(c rune, echo bool) {
|
|
|
|
|
if c < 9 || (c > 10 && c < 13) || (c > 13 && c < 32) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
@ -93,12 +163,12 @@ func max(a, b int) int {
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (lb Buffer) printBuf() {
|
|
|
|
|
func (lb buffer) printBuf() {
|
|
|
|
|
out := lb.buf[lb.offset:min(len(lb.buf), lb.offset+lb.maxWidth)]
|
|
|
|
|
fmt.Printf("\r%s%s\033[0K\r\033[%dC", lb.prompt, string(out), lb.cursor - lb.offset + len(lb.prompt))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (lb *Buffer) controlInput(c rune) {
|
|
|
|
|
func (lb *buffer) controlInput(c rune) {
|
|
|
|
|
switch c {
|
|
|
|
|
case Delete:
|
|
|
|
|
lb.deleteChar()
|
|
|
|
@ -115,7 +185,7 @@ func (lb *Buffer) controlInput(c rune) {
|
|
|
|
|
lb.cursor--
|
|
|
|
|
}
|
|
|
|
|
case RightArrow:
|
|
|
|
|
// FIXME: This still isnt working quite right...
|
|
|
|
|
// This is still mildly funky, but works enough
|
|
|
|
|
for ;lb.cursor - lb.offset >= lb.maxWidth && lb.cursor < len(lb.buf); {
|
|
|
|
|
lb.offset++
|
|
|
|
|
}
|
|
|
|
@ -123,18 +193,16 @@ func (lb *Buffer) controlInput(c rune) {
|
|
|
|
|
lb.cursor++
|
|
|
|
|
}
|
|
|
|
|
case Home:
|
|
|
|
|
lb.offset = 0
|
|
|
|
|
lb.cursor = 0
|
|
|
|
|
lb.offset = 0
|
|
|
|
|
lb.cursor = 0
|
|
|
|
|
case End:
|
|
|
|
|
if lb.cursor < len(lb.buf) {
|
|
|
|
|
lb.cursor = len(lb.buf)
|
|
|
|
|
lb.offset = max(lb.cursor - lb.maxWidth, 0)
|
|
|
|
|
}
|
|
|
|
|
lb.cursor = len(lb.buf)
|
|
|
|
|
lb.offset = max(lb.cursor - lb.maxWidth, 0)
|
|
|
|
|
}
|
|
|
|
|
lb.printBuf()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (lb *Buffer) seedContent(s string) {
|
|
|
|
|
func (lb *buffer) seedContent(s string) {
|
|
|
|
|
for _, r := range s {
|
|
|
|
|
lb.addChar(r, false)
|
|
|
|
|
}
|
|
|
|
@ -147,41 +215,6 @@ func parseCursorPosition(esc string) (int, int, error) {
|
|
|
|
|
return row, col, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func GetText(prompt string, content string) string {
|
|
|
|
|
cols, _ := termios.GetWindowSize()
|
|
|
|
|
b := Buffer{make([]rune, 0, (len(content)+1)*2), 0, cols-len(prompt), 0, 0, prompt}
|
|
|
|
|
b.seedContent(content)
|
|
|
|
|
|
|
|
|
|
fmt.Print(prompt)
|
|
|
|
|
fmt.Print("\033[6n")
|
|
|
|
|
b.printBuf()
|
|
|
|
|
|
|
|
|
|
var ch rune
|
|
|
|
|
var err error
|
|
|
|
|
reader := bufio.NewReader(os.Stdin)
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
ch, err = readKey(reader)
|
|
|
|
|
if err != nil && err.Error() == "response" {
|
|
|
|
|
b.cursorStart = int(ch)
|
|
|
|
|
b.maxWidth = cols - b.cursorStart
|
|
|
|
|
} else if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if ch == CarriageReturn || ch == NewLine {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if isControl(ch) {
|
|
|
|
|
b.controlInput(ch)
|
|
|
|
|
} else {
|
|
|
|
|
b.addChar(ch, true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
return b.String()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func readKey(reader *bufio.Reader) (rune, error) {
|
|
|
|
|
char, _, err := reader.ReadRune()
|
|
|
|
@ -220,13 +253,12 @@ func readKey(reader *bufio.Reader) (rune, error) {
|
|
|
|
|
case isDeleteKey(escSeq):
|
|
|
|
|
char = Delete
|
|
|
|
|
case escSeq[len(escSeq)-1] == 'R':
|
|
|
|
|
// This is a request for cursor position
|
|
|
|
|
_, cols, err := parseCursorPosition(escSeq)
|
|
|
|
|
if err == nil {
|
|
|
|
|
err = fmt.Errorf("response")
|
|
|
|
|
}
|
|
|
|
|
return rune(cols), err
|
|
|
|
|
default:
|
|
|
|
|
fmt.Printf("Odd sequence: %s\n", escSeq[1:])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return char, nil
|
|
|
|
@ -268,46 +300,3 @@ func isEndKey(seq string) bool {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func PrintKey(k rune) {
|
|
|
|
|
switch true {
|
|
|
|
|
case k == UpArrow:
|
|
|
|
|
fmt.Println("UpArrow")
|
|
|
|
|
case k == DownArrow:
|
|
|
|
|
fmt.Println("DownArrow")
|
|
|
|
|
case k == LeftArrow:
|
|
|
|
|
fmt.Println("LeftArrow")
|
|
|
|
|
case k == RightArrow:
|
|
|
|
|
fmt.Println("RightArrow")
|
|
|
|
|
case k == Escape:
|
|
|
|
|
fmt.Println("Escape")
|
|
|
|
|
case k == NewLine:
|
|
|
|
|
fmt.Println("NewLine")
|
|
|
|
|
case k == CarriageReturn:
|
|
|
|
|
fmt.Println("CarriageReturn")
|
|
|
|
|
case k == Delete:
|
|
|
|
|
fmt.Println("Delete")
|
|
|
|
|
case k == Home:
|
|
|
|
|
fmt.Println("Home")
|
|
|
|
|
case k == End:
|
|
|
|
|
fmt.Println("End")
|
|
|
|
|
case k == BackSpace:
|
|
|
|
|
fmt.Println("BackSpace")
|
|
|
|
|
default:
|
|
|
|
|
fmt.Printf("%c (%d)\n", k, k)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
// This main func is just for testing and will
|
|
|
|
|
// be removed with the intention that this just
|
|
|
|
|
// gets used as a lib
|
|
|
|
|
termios.SetCharMode()
|
|
|
|
|
defer termios.Restore()
|
|
|
|
|
str := ""
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
fmt.Print("\n")
|
|
|
|
|
str = GetText("> ", str)
|
|
|
|
|
}
|
|
|
|
|
}
|