diff --git a/char.go b/char.go index 7885da3..3e23865 100644 --- a/char.go +++ b/char.go @@ -20,6 +20,7 @@ const ( CharTranspose = 20 CharCtrlU = 21 CharCtrlW = 23 + CharCtrlZ = 26 CharEsc = 27 CharEscapeEx = 91 CharBackspace = 127 diff --git a/operation.go b/operation.go index e4fa8f5..135928c 100644 --- a/operation.go +++ b/operation.go @@ -186,6 +186,10 @@ func (o *Operation) ioloop() { if o.IsInCompleteMode() { o.OnComplete() } + case CharCtrlZ: + o.buf.Clean() + o.t.SleepToResume() + o.Refresh() case MetaBackspace, CharCtrlW: o.buf.BackEscapeWord() case CharEnter, CharCtrlJ: diff --git a/terminal.go b/terminal.go index b1e278b..486d9a3 100644 --- a/terminal.go +++ b/terminal.go @@ -3,6 +3,7 @@ package readline import ( "bufio" "fmt" + "strings" "sync" "sync/atomic" ) @@ -15,6 +16,7 @@ type Terminal struct { kickChan chan struct{} wg sync.WaitGroup isReading int32 + sleeping int32 } func NewTerminal(cfg *Config) (*Terminal, error) { @@ -32,6 +34,20 @@ func NewTerminal(cfg *Config) (*Terminal, error) { 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() } @@ -99,6 +115,10 @@ func (t *Terminal) ioloop() { expectNextChar = false r, _, err := buf.ReadRune() if err != nil { + if strings.Contains(err.Error(), "interrupted system call") { + expectNextChar = true + continue + } break } diff --git a/utils.go b/utils.go index 7ec052c..932b10e 100644 --- a/utils.go +++ b/utils.go @@ -4,6 +4,8 @@ import ( "bufio" "bytes" "strconv" + "sync" + "time" "golang.org/x/crypto/ssh/terminal" ) @@ -12,6 +14,31 @@ var ( isWindows = false ) +// WaitForResume need to call before current process got suspend. +// It will run a ticker until a long duration is occurs, +// which means this process is resumed. +func WaitForResume() chan struct{} { + ch := make(chan struct{}) + var wg sync.WaitGroup + wg.Add(1) + go func() { + ticker := time.NewTicker(10 * time.Millisecond) + t := time.Now() + wg.Done() + for { + now := <-ticker.C + if now.Sub(t) > 100*time.Millisecond { + break + } + t = now + } + ticker.Stop() + ch <- struct{}{} + }() + wg.Wait() + return ch +} + // IsTerminal returns true if the given file descriptor is a terminal. func IsTerminal(fd int) bool { return terminal.IsTerminal(fd) diff --git a/utils_unix.go b/utils_unix.go index f0f0da6..2c9bc8e 100644 --- a/utils_unix.go +++ b/utils_unix.go @@ -17,6 +17,16 @@ type winsize struct { Ypixel uint16 } +// SuspendMe use to send suspend signal to myself, when we in the raw mode. +// For OSX it need to send to parent's pid +// For Linux it need to send to myself +func SuspendMe() { + p, _ := os.FindProcess(os.Getppid()) + p.Signal(syscall.SIGTSTP) + p, _ = os.FindProcess(os.Getpid()) + p.Signal(syscall.SIGTSTP) +} + // get width of the terminal func getWidth(stdoutFd int) int { ws := &winsize{} diff --git a/utils_windows.go b/utils_windows.go index f88d038..d82d577 100644 --- a/utils_windows.go +++ b/utils_windows.go @@ -4,6 +4,9 @@ package readline import "syscall" +func SuspendMe() { +} + func GetStdin() int { return int(syscall.Stdin) }