2023-10-25 23:41:18 +00:00
|
|
|
package readline
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"syscall"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Prompt struct {
|
|
|
|
Prompt string
|
|
|
|
AltPrompt string
|
|
|
|
Placeholder string
|
|
|
|
AltPlaceholder string
|
|
|
|
UseAlt bool
|
|
|
|
}
|
|
|
|
|
|
|
|
type Terminal struct {
|
|
|
|
outchan chan rune
|
|
|
|
}
|
|
|
|
|
|
|
|
type Instance struct {
|
|
|
|
Prompt *Prompt
|
|
|
|
Terminal *Terminal
|
|
|
|
History *History
|
2023-11-26 04:30:34 +00:00
|
|
|
Pasting bool
|
2023-10-25 23:41:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func New(prompt Prompt) (*Instance, error) {
|
|
|
|
term, err := NewTerminal()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
history, err := NewHistory()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Instance{
|
|
|
|
Prompt: &prompt,
|
|
|
|
Terminal: term,
|
|
|
|
History: history,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *Instance) Readline() (string, error) {
|
|
|
|
prompt := i.Prompt.Prompt
|
2023-11-26 04:30:34 +00:00
|
|
|
if i.Prompt.UseAlt || i.Pasting {
|
2023-10-25 23:41:18 +00:00
|
|
|
prompt = i.Prompt.AltPrompt
|
|
|
|
}
|
2023-10-28 03:01:48 +00:00
|
|
|
fmt.Print(prompt)
|
2023-10-25 23:41:18 +00:00
|
|
|
|
2023-10-30 20:18:12 +00:00
|
|
|
fd := int(syscall.Stdin)
|
|
|
|
termios, err := SetRawMode(fd)
|
2023-10-25 23:41:18 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2023-10-30 20:18:12 +00:00
|
|
|
defer UnsetRawMode(fd, termios)
|
2023-10-25 23:41:18 +00:00
|
|
|
|
|
|
|
buf, _ := NewBuffer(i.Prompt)
|
|
|
|
|
|
|
|
var esc bool
|
|
|
|
var escex bool
|
|
|
|
var metaDel bool
|
|
|
|
|
|
|
|
var currentLineBuf []rune
|
|
|
|
|
|
|
|
for {
|
2023-11-26 04:30:34 +00:00
|
|
|
// don't show placeholder when pasting unless we're in multiline mode
|
|
|
|
showPlaceholder := !i.Pasting || i.Prompt.UseAlt
|
|
|
|
if buf.IsEmpty() && showPlaceholder {
|
2023-10-25 23:41:18 +00:00
|
|
|
ph := i.Prompt.Placeholder
|
|
|
|
if i.Prompt.UseAlt {
|
|
|
|
ph = i.Prompt.AltPlaceholder
|
|
|
|
}
|
|
|
|
fmt.Printf(ColorGrey + ph + fmt.Sprintf(CursorLeftN, len(ph)) + ColorDefault)
|
|
|
|
}
|
|
|
|
|
2023-10-28 03:01:48 +00:00
|
|
|
r, err := i.Terminal.Read()
|
2023-10-25 23:41:18 +00:00
|
|
|
|
2023-10-28 03:01:48 +00:00
|
|
|
if buf.IsEmpty() {
|
|
|
|
fmt.Print(ClearToEOL)
|
2023-10-25 23:41:18 +00:00
|
|
|
}
|
|
|
|
|
2023-10-28 04:26:23 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", io.EOF
|
|
|
|
}
|
|
|
|
|
2023-10-25 23:41:18 +00:00
|
|
|
if escex {
|
|
|
|
escex = false
|
|
|
|
|
|
|
|
switch r {
|
|
|
|
case KeyUp:
|
|
|
|
if i.History.Pos > 0 {
|
|
|
|
if i.History.Pos == i.History.Size() {
|
|
|
|
currentLineBuf = []rune(buf.String())
|
|
|
|
}
|
|
|
|
buf.Replace(i.History.Prev())
|
|
|
|
}
|
|
|
|
case KeyDown:
|
|
|
|
if i.History.Pos < i.History.Size() {
|
|
|
|
buf.Replace(i.History.Next())
|
|
|
|
if i.History.Pos == i.History.Size() {
|
|
|
|
buf.Replace(currentLineBuf)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case KeyLeft:
|
|
|
|
buf.MoveLeft()
|
|
|
|
case KeyRight:
|
|
|
|
buf.MoveRight()
|
|
|
|
case CharBracketedPaste:
|
2023-10-26 22:57:00 +00:00
|
|
|
var code string
|
|
|
|
for cnt := 0; cnt < 3; cnt++ {
|
2023-10-28 03:01:48 +00:00
|
|
|
r, err = i.Terminal.Read()
|
|
|
|
if err != nil {
|
|
|
|
return "", io.EOF
|
|
|
|
}
|
|
|
|
|
2023-10-26 22:57:00 +00:00
|
|
|
code += string(r)
|
|
|
|
}
|
|
|
|
if code == CharBracketedPasteStart {
|
2023-11-26 04:30:34 +00:00
|
|
|
i.Pasting = true
|
2023-10-26 22:57:00 +00:00
|
|
|
} else if code == CharBracketedPasteEnd {
|
2023-11-26 04:30:34 +00:00
|
|
|
i.Pasting = false
|
2023-10-26 22:57:00 +00:00
|
|
|
}
|
2023-10-25 23:41:18 +00:00
|
|
|
case KeyDel:
|
|
|
|
if buf.Size() > 0 {
|
|
|
|
buf.Delete()
|
|
|
|
}
|
|
|
|
metaDel = true
|
|
|
|
case MetaStart:
|
|
|
|
buf.MoveToStart()
|
|
|
|
case MetaEnd:
|
|
|
|
buf.MoveToEnd()
|
|
|
|
default:
|
|
|
|
// skip any keys we don't know about
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
} else if esc {
|
|
|
|
esc = false
|
|
|
|
|
|
|
|
switch r {
|
|
|
|
case 'b':
|
|
|
|
buf.MoveLeftWord()
|
|
|
|
case 'f':
|
|
|
|
buf.MoveRightWord()
|
2023-11-21 20:26:47 +00:00
|
|
|
case CharBackspace:
|
|
|
|
buf.DeleteWord()
|
2023-10-25 23:41:18 +00:00
|
|
|
case CharEscapeEx:
|
|
|
|
escex = true
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
switch r {
|
2023-10-28 03:01:48 +00:00
|
|
|
case CharNull:
|
|
|
|
continue
|
2023-10-25 23:41:18 +00:00
|
|
|
case CharEsc:
|
|
|
|
esc = true
|
|
|
|
case CharInterrupt:
|
|
|
|
return "", ErrInterrupt
|
|
|
|
case CharLineStart:
|
|
|
|
buf.MoveToStart()
|
|
|
|
case CharLineEnd:
|
|
|
|
buf.MoveToEnd()
|
|
|
|
case CharBackward:
|
|
|
|
buf.MoveLeft()
|
|
|
|
case CharForward:
|
|
|
|
buf.MoveRight()
|
|
|
|
case CharBackspace, CharCtrlH:
|
|
|
|
buf.Remove()
|
|
|
|
case CharTab:
|
|
|
|
// todo: convert back to real tabs
|
|
|
|
for cnt := 0; cnt < 8; cnt++ {
|
|
|
|
buf.Add(' ')
|
|
|
|
}
|
|
|
|
case CharDelete:
|
|
|
|
if buf.Size() > 0 {
|
|
|
|
buf.Delete()
|
|
|
|
} else {
|
|
|
|
return "", io.EOF
|
|
|
|
}
|
|
|
|
case CharKill:
|
|
|
|
buf.DeleteRemaining()
|
|
|
|
case CharCtrlU:
|
|
|
|
buf.DeleteBefore()
|
|
|
|
case CharCtrlL:
|
|
|
|
buf.ClearScreen()
|
|
|
|
case CharCtrlW:
|
|
|
|
buf.DeleteWord()
|
|
|
|
case CharEnter:
|
2023-10-26 22:57:00 +00:00
|
|
|
output := buf.String()
|
|
|
|
if output != "" {
|
|
|
|
i.History.Add([]rune(output))
|
|
|
|
}
|
|
|
|
buf.MoveToEnd()
|
|
|
|
fmt.Println()
|
2023-11-26 04:30:34 +00:00
|
|
|
|
2023-10-26 22:57:00 +00:00
|
|
|
return output, nil
|
2023-10-25 23:41:18 +00:00
|
|
|
default:
|
|
|
|
if metaDel {
|
|
|
|
metaDel = false
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if r >= CharSpace || r == CharEnter {
|
|
|
|
buf.Add(r)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *Instance) HistoryEnable() {
|
|
|
|
i.History.Enabled = true
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *Instance) HistoryDisable() {
|
|
|
|
i.History.Enabled = false
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewTerminal() (*Terminal, error) {
|
|
|
|
t := &Terminal{
|
|
|
|
outchan: make(chan rune),
|
|
|
|
}
|
|
|
|
|
|
|
|
go t.ioloop()
|
|
|
|
|
|
|
|
return t, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *Terminal) ioloop() {
|
|
|
|
buf := bufio.NewReader(os.Stdin)
|
|
|
|
|
|
|
|
for {
|
|
|
|
r, _, err := buf.ReadRune()
|
|
|
|
if err != nil {
|
2023-10-28 03:26:04 +00:00
|
|
|
close(t.outchan)
|
2023-10-25 23:41:18 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
t.outchan <- r
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-28 03:01:48 +00:00
|
|
|
func (t *Terminal) Read() (rune, error) {
|
2023-10-25 23:41:18 +00:00
|
|
|
r, ok := <-t.outchan
|
|
|
|
if !ok {
|
2023-10-28 03:01:48 +00:00
|
|
|
return 0, io.EOF
|
2023-10-25 23:41:18 +00:00
|
|
|
}
|
2023-10-28 03:01:48 +00:00
|
|
|
|
|
|
|
return r, nil
|
2023-10-25 23:41:18 +00:00
|
|
|
}
|