1
1
Fork 0
qline/qline.go

314 lines
5.7 KiB
Go

package main
import (
"bufio"
"fmt"
"os"
"strings"
"tildegit.org/sloum/qline/termios"
)
const (
UpArrow rune = iota - 20
DownArrow
LeftArrow
RightArrow
Delete
Home
End
PrintScreen
PageUp
PageDown
Escape rune = 27
NewLine rune = 10
CarriageReturn rune = 13
BackSpace rune = 127
)
var (
width int
)
type Buffer struct {
buf []rune
cursor int
maxWidth int
offset int
cursorStart int
prompt string
}
func (lb Buffer) String() string {
return string(lb.buf)
}
func (lb *Buffer) deleteChar() {
if lb.cursor == len(lb.buf) {
return
} else if lb.cursor == len(lb.buf)-1 {
lb.buf = lb.buf[:len(lb.buf)-1]
} else {
lb.buf = append(lb.buf[:lb.cursor], lb.buf[lb.cursor+1:]...)
}
if lb.offset > 0 {
lb.offset--
}
}
// 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) {
if c < 9 || (c > 10 && c < 13) || (c > 13 && c < 32) {
return
}
if lb.cursor == len(lb.buf) {
lb.buf = append(lb.buf, c)
lb.cursor++
} else {
lb.buf = append(lb.buf[:lb.cursor+1], lb.buf[lb.cursor:]...)
lb.buf[lb.cursor] = c
lb.cursor++
}
if lb.cursor - lb.offset > lb.maxWidth {
lb.offset++
}
if echo {
lb.printBuf()
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
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) {
switch c {
case Delete:
lb.deleteChar()
case BackSpace:
if lb.cursor > 0 {
lb.cursor--
lb.deleteChar()
}
case LeftArrow:
if lb.offset > 0 && lb.cursor - lb.offset == 0 {
lb.offset--
}
if lb.cursor > 0 {
lb.cursor--
}
case RightArrow:
// FIXME: This still isnt working quite right...
for ;lb.cursor - lb.offset >= lb.maxWidth && lb.cursor < len(lb.buf); {
lb.offset++
}
if lb.cursor < len(lb.buf) {
lb.cursor++
}
case Home:
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.printBuf()
}
func (lb *Buffer) seedContent(s string) {
for _, r := range s {
lb.addChar(r, false)
}
}
func parseCursorPosition(esc string) (int, int, error) {
var row, col int
r := strings.NewReader(esc)
_, err := fmt.Fscanf(r, "\033[%d;%dR", &row, &col)
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()
if err != nil {
return 0, err
}
avail := reader.Buffered()
if char == Escape && avail > 0 {
var b strings.Builder
b.WriteRune(27)
for ; avail > 0; avail-- {
c, _, e := reader.ReadRune()
if e != nil {
break
}
b.WriteRune(c)
}
escSeq := b.String()
switch true {
case escSeq == "\033[A":
char = UpArrow
case escSeq == "\033[B":
char = DownArrow
case escSeq == "\033[C":
char = RightArrow
case escSeq == "\033[D":
char = LeftArrow
case escSeq == "\033[5~":
char = PageUp
case escSeq == "\033[6~":
char = PageDown
case isHomeKey(escSeq):
char = Home
case isEndKey(escSeq):
char = End
case isDeleteKey(escSeq):
char = Delete
case escSeq[len(escSeq)-1] == 'R':
_, 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
}
func isControl(c rune) bool {
switch c {
case UpArrow, DownArrow, RightArrow, LeftArrow, PageUp, PageDown, Home, End, Delete, BackSpace, CarriageReturn, NewLine, Escape:
return true
default:
return false
}
}
func isDeleteKey(seq string) bool {
switch seq {
case "\033[3~", "\033[P":
return true
default:
return false
}
}
func isHomeKey(seq string) bool {
switch seq {
case "\033[1~", "\033[7~", "\033[H", "\033OH":
return true
default:
return false
}
}
func isEndKey(seq string) bool {
switch seq {
case "\033[4~", "\033[8~", "\033[F", "\033OF":
return true
default:
return false
}
}
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)
}
}