package readline import ( "bufio" "fmt" "io" "strings" "sync" "sync/atomic" ) type Terminal struct { m sync.Mutex cfg *Config outchan chan rune closed int32 stopChan chan struct{} kickChan chan struct{} wg sync.WaitGroup isReading int32 sleeping int32 sizeChan chan string } func NewTerminal(cfg *Config) (*Terminal, error) { if err := cfg.Init(); err != nil { return nil, err } t := &Terminal{ cfg: cfg, kickChan: make(chan struct{}, 1), outchan: make(chan rune), stopChan: make(chan struct{}, 1), sizeChan: make(chan string, 1), } go t.ioloop() return t, nil } // SleepToResume will sleep myself, and return only if I'm resumed. func (t *Terminal) SleepToResume() { if !atomic.CompareAndSwapInt32(&t.sleeping, 0, 1) { return } defer atomic.StoreInt32(&t.sleeping, 0) t.ExitRawMode() ch := WaitForResume() SuspendMe() <-ch t.EnterRawMode() } func (t *Terminal) EnterRawMode() (err error) { return t.cfg.FuncMakeRaw() } func (t *Terminal) ExitRawMode() (err error) { return t.cfg.FuncExitRaw() } func (t *Terminal) Write(b []byte) (int, error) { return t.cfg.Stdout.Write(b) } // WriteStdin prefill the next Stdin fetch // Next time you call ReadLine() this value will be writen before the user input func (t *Terminal) WriteStdin(b []byte) (int, error) { return t.cfg.StdinWriter.Write(b) } type termSize struct { left int top int } func (t *Terminal) GetOffset(f func(offset string)) { go func() { f(<-t.sizeChan) }() t.Write([]byte("\033[6n")) } func (t *Terminal) Print(s string) { fmt.Fprintf(t.cfg.Stdout, "%s", s) } func (t *Terminal) PrintRune(r rune) { fmt.Fprintf(t.cfg.Stdout, "%c", r) } func (t *Terminal) Readline() *Operation { return NewOperation(t, t.cfg) } // return rune(0) if meet EOF func (t *Terminal) ReadRune() rune { ch, ok := <-t.outchan if !ok { return rune(0) } return ch } func (t *Terminal) IsReading() bool { return atomic.LoadInt32(&t.isReading) == 1 } func (t *Terminal) KickRead() { select { case t.kickChan <- struct{}{}: default: } } func (t *Terminal) ioloop() { t.wg.Add(1) defer func() { t.wg.Done() close(t.outchan) }() var ( isEscape bool isEscapeEx bool expectNextChar bool ) buf := bufio.NewReader(t.getStdin()) for { if !expectNextChar { atomic.StoreInt32(&t.isReading, 0) select { case <-t.kickChan: atomic.StoreInt32(&t.isReading, 1) case <-t.stopChan: return } } expectNextChar = false r, _, err := buf.ReadRune() if err != nil { if strings.Contains(err.Error(), "interrupted system call") { expectNextChar = true continue } break } if isEscape { isEscape = false if r == CharEscapeEx { expectNextChar = true isEscapeEx = true continue } r = escapeKey(r, buf) } else if isEscapeEx { isEscapeEx = false if key := readEscKey(r, buf); key != nil { r = escapeExKey(key) // offset if key.typ == 'R' { if _, _, ok := key.Get2(); ok { select { case t.sizeChan <- key.attr: default: } } expectNextChar = true continue } } if r == 0 { expectNextChar = true continue } } expectNextChar = true switch r { case CharEsc: if t.cfg.VimMode { t.outchan <- r break } isEscape = true case CharInterrupt, CharEnter, CharCtrlJ, CharDelete: expectNextChar = false fallthrough default: t.outchan <- r } } } func (t *Terminal) Bell() { fmt.Fprintf(t, "%c", CharBell) } func (t *Terminal) Close() error { if atomic.SwapInt32(&t.closed, 1) != 0 { return nil } if closer, ok := t.cfg.Stdin.(io.Closer); ok { closer.Close() } close(t.stopChan) t.wg.Wait() return t.ExitRawMode() } func (t *Terminal) GetConfig() *Config { t.m.Lock() cfg := *t.cfg t.m.Unlock() return &cfg } func (t *Terminal) getStdin() io.Reader { t.m.Lock() r := t.cfg.Stdin t.m.Unlock() return r } func (t *Terminal) SetConfig(c *Config) error { if err := c.Init(); err != nil { return err } t.m.Lock() t.cfg = c t.m.Unlock() return nil }