123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- package readline
- import (
- "bufio"
- "fmt"
- "io"
- "os"
- )
- type Prompt struct {
- Prompt string
- AltPrompt string
- Placeholder string
- AltPlaceholder string
- UseAlt bool
- }
- func (p *Prompt) prompt() string {
- if p.UseAlt {
- return p.AltPrompt
- }
- return p.Prompt
- }
- func (p *Prompt) placeholder() string {
- if p.UseAlt {
- return p.AltPlaceholder
- }
- return p.Placeholder
- }
- type Terminal struct {
- outchan chan rune
- rawmode bool
- termios any
- }
- type Instance struct {
- Prompt *Prompt
- Terminal *Terminal
- History *History
- Pasting bool
- }
- 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) {
- if !i.Terminal.rawmode {
- fd := os.Stdin.Fd()
- termios, err := SetRawMode(fd)
- if err != nil {
- return "", err
- }
- i.Terminal.rawmode = true
- i.Terminal.termios = termios
- }
- prompt := i.Prompt.prompt()
- if i.Pasting {
- // force alt prompt when pasting
- prompt = i.Prompt.AltPrompt
- }
- fmt.Print(prompt)
- defer func() {
- fd := os.Stdin.Fd()
- //nolint:errcheck
- UnsetRawMode(fd, i.Terminal.termios)
- i.Terminal.rawmode = false
- }()
- buf, _ := NewBuffer(i.Prompt)
- var esc bool
- var escex bool
- var metaDel bool
- var currentLineBuf []rune
- for {
- // don't show placeholder when pasting unless we're in multiline mode
- showPlaceholder := !i.Pasting || i.Prompt.UseAlt
- if buf.IsEmpty() && showPlaceholder {
- ph := i.Prompt.placeholder()
- fmt.Print(ColorGrey + ph + CursorLeftN(len(ph)) + ColorDefault)
- }
- r, err := i.Terminal.Read()
- if buf.IsEmpty() {
- fmt.Print(ClearToEOL)
- }
- if err != nil {
- return "", io.EOF
- }
- 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:
- var code string
- for range 3 {
- r, err = i.Terminal.Read()
- if err != nil {
- return "", io.EOF
- }
- code += string(r)
- }
- if code == CharBracketedPasteStart {
- i.Pasting = true
- } else if code == CharBracketedPasteEnd {
- i.Pasting = false
- }
- case KeyDel:
- if buf.DisplaySize() > 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()
- case CharBackspace:
- buf.DeleteWord()
- case CharEscapeEx:
- escex = true
- }
- continue
- }
- switch r {
- case CharNull:
- continue
- 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 range 8 {
- buf.Add(' ')
- }
- case CharDelete:
- if buf.DisplaySize() > 0 {
- buf.Delete()
- } else {
- return "", io.EOF
- }
- case CharKill:
- buf.DeleteRemaining()
- case CharCtrlU:
- buf.DeleteBefore()
- case CharCtrlL:
- buf.ClearScreen()
- case CharCtrlW:
- buf.DeleteWord()
- case CharCtrlZ:
- fd := os.Stdin.Fd()
- return handleCharCtrlZ(fd, i.Terminal.termios)
- case CharEnter, CharCtrlJ:
- output := buf.String()
- if output != "" {
- i.History.Add([]rune(output))
- }
- buf.MoveToEnd()
- fmt.Println()
- return output, nil
- default:
- if metaDel {
- metaDel = false
- continue
- }
- if r >= CharSpace || r == CharEnter || r == CharCtrlJ {
- buf.Add(r)
- }
- }
- }
- }
- func (i *Instance) HistoryEnable() {
- i.History.Enabled = true
- }
- func (i *Instance) HistoryDisable() {
- i.History.Enabled = false
- }
- func NewTerminal() (*Terminal, error) {
- fd := os.Stdin.Fd()
- termios, err := SetRawMode(fd)
- if err != nil {
- return nil, err
- }
- t := &Terminal{
- outchan: make(chan rune),
- rawmode: true,
- termios: termios,
- }
- go t.ioloop()
- return t, nil
- }
- func (t *Terminal) ioloop() {
- buf := bufio.NewReader(os.Stdin)
- for {
- r, _, err := buf.ReadRune()
- if err != nil {
- close(t.outchan)
- break
- }
- t.outchan <- r
- }
- }
- func (t *Terminal) Read() (rune, error) {
- r, ok := <-t.outchan
- if !ok {
- return 0, io.EOF
- }
- return r, nil
- }
|