Skip to content

Commit

Permalink
fixes #452 Lost a key event once when exiting or suspending in v2.2.1.
Browse files Browse the repository at this point in the history
fixes #449 Lost keyboard input after suspend on Windows 10 PowerShell
fixes #148 Make tcell usable with any io.Reader and io.Writer

This introduces a new Tty interface so that applications can supply
their own implementation.  This should facilitate work for applications
that wish to provide e.g. a webasm version of the terminal, or that need
to use different kinds of file plumbing.
  • Loading branch information
gdamore committed May 16, 2021
1 parent 8f925d8 commit 97c0480
Show file tree
Hide file tree
Showing 8 changed files with 339 additions and 197 deletions.
29 changes: 10 additions & 19 deletions nonblock_bsd.go
Expand Up @@ -17,34 +17,25 @@
package tcell

import (
"os"
"syscall"

"golang.org/x/sys/unix"
)

// BSD systems use TIOC style ioctls.

// nonBlocking changes VMIN to 0, and VTIME to 1. This basically ensures that
// we can wake up the input loop. We only want to do this if we are going to interrupt
// that loop. Normally we use VMIN 1 and VTIME 0, which ensures we pick up bytes when
// they come but don't spin burning cycles.
func (t *tScreen) nonBlocking(on bool) {
fd := int(os.Stdin.Fd())
// tcSetBufParams is used by the tty driver on UNIX systems to configure the
// buffering parameters (minimum character count and minimum wait time in msec.)
func tcSetBufParams(fd int, vMin uint8, vTime uint8) error {
_ = syscall.SetNonblock(fd, true)
tio, err := unix.IoctlGetTermios(fd, unix.TIOCGETA)
if err != nil {
return
return err
}
if on {
tio.Cc[unix.VMIN] = 0
tio.Cc[unix.VTIME] = 0
} else {
// block for any output
tio.Cc[unix.VTIME] = 0
tio.Cc[unix.VMIN] = 1
tio.Cc[unix.VMIN] = vMin
tio.Cc[unix.VTIME] = vTime
if err = unix.IoctlSetTermios(fd, unix.TIOCSETA, tio); err != nil {
return err
}

_ = syscall.SetNonblock(fd, on)
// We want to set this *right now*.
_ = unix.IoctlSetTermios(fd, unix.TIOCSETA, tio)
return nil
}
21 changes: 0 additions & 21 deletions nonblock_stub.go

This file was deleted.

33 changes: 10 additions & 23 deletions nonblock_unix.go
Expand Up @@ -17,36 +17,23 @@
package tcell

import (
"os"
"syscall"

"golang.org/x/sys/unix"
)

// NB: We might someday wish to move Windows to this model. However,
// that would probably mean sacrificing some of the richer key reporting
// that we can obtain with the console API present on Windows.

// nonBlocking changes VMIN to 0, and VTIME to 1. This basically ensures that
// we can wake up the input loop. We only want to do this if we are going to interrupt
// that loop. Normally we use VMIN 1 and VTIME 0, which ensures we pick up bytes when
// they come but don't spin burning cycles.
func (t *tScreen) nonBlocking(on bool) {
fd := int(os.Stdin.Fd())
// tcSetBufParams is used by the tty driver on UNIX systems to configure the
// buffering parameters (minimum character count and minimum wait time in msec.)
func tcSetBufParams(fd int, vMin uint8, vTime uint8) error {
_ = syscall.SetNonblock(fd, true)
tio, err := unix.IoctlGetTermios(fd, unix.TCGETS)
if err != nil {
return
return err
}
if on {
tio.Cc[unix.VMIN] = 0
tio.Cc[unix.VTIME] = 0
} else {
// block for any output
tio.Cc[unix.VTIME] = 0
tio.Cc[unix.VMIN] = 1
tio.Cc[unix.VMIN] = vMin
tio.Cc[unix.VTIME] = vTime
if err = unix.IoctlSetTermios(fd, unix.TCSETS, tio); err != nil {
return err
}

_ = syscall.SetNonblock(fd, on)
// We want to set this *right now*.
_ = unix.IoctlSetTermios(fd, unix.TCSETS, tio)
return nil
}
104 changes: 95 additions & 9 deletions tscreen.go
Expand Up @@ -16,6 +16,7 @@ package tcell

import (
"bytes"
"errors"
"io"
"os"
"strconv"
Expand All @@ -42,6 +43,14 @@ import (
// $COLUMNS environment variables can be set to the actual window size,
// otherwise defaults taken from the terminal database are used.
func NewTerminfoScreen() (Screen, error) {
return NewTerminfoScreenFromTty(nil)
}

// NewTerminfoScreenFromTty returns a Screen using a custom Tty implementation.
// If the passed in tty is nil, then a reasonable default (typically /dev/tty)
// is presumed, at least on UNIX hosts. (Windows hosts will typically fail this
// call altogether.)
func NewTerminfoScreenFromTty(tty Tty) (Screen, error) {
ti, e := terminfo.LookupTerminfo(os.Getenv("TERM"))
if e != nil {
ti, e = loadDynamicTerminfo(os.Getenv("TERM"))
Expand All @@ -50,7 +59,7 @@ func NewTerminfoScreen() (Screen, error) {
}
terminfo.AddTerminfo(ti)
}
t := &tScreen{ti: ti}
t := &tScreen{ti: ti, tty: tty}

t.keyexist = make(map[Key]bool)
t.keycodes = make(map[string]*tKeyCode)
Expand All @@ -77,12 +86,11 @@ type tKeyCode struct {
// tScreen represents a screen backed by a terminfo implementation.
type tScreen struct {
ti *terminfo.Terminfo
tty Tty
h int
w int
fini bool
cells CellBuffer
in *os.File
out *os.File
buffering bool // true if we are collecting writes to buf instead of sending directly to out
buf bytes.Buffer
curstyle Style
Expand Down Expand Up @@ -731,15 +739,15 @@ func (t *tScreen) writeString(s string) {
if t.buffering {
_, _ = io.WriteString(&t.buf, s)
} else {
_, _ = io.WriteString(t.out, s)
_, _ = io.WriteString(t.tty, s)
}
}

func (t *tScreen) TPuts(s string) {
if t.buffering {
t.ti.TPuts(&t.buf, s)
} else {
t.ti.TPuts(t.out, s)
t.ti.TPuts(t.tty, s)
}
}

Expand Down Expand Up @@ -807,7 +815,7 @@ func (t *tScreen) draw() {
// restore the cursor
t.showCursor()

_, _ = t.buf.WriteTo(t.out)
_, _ = t.buf.WriteTo(t.tty)
}

func (t *tScreen) EnableMouse(flags ...MouseFlags) {
Expand Down Expand Up @@ -885,7 +893,7 @@ func (t *tScreen) Size() (int, int) {
}

func (t *tScreen) resize() {
if w, h, e := t.getWinSize(); e == nil {
if w, h, e := t.tty.WindowSize(); e == nil {
if w != t.w || h != t.h {
t.cx = -1
t.cy = -1
Expand Down Expand Up @@ -1493,7 +1501,7 @@ func (t *tScreen) inputLoop(stopQ chan struct{}) {
default:
}
chunk := make([]byte, 128)
n, e := t.in.Read(chunk)
n, e := t.tty.Read(chunk)
switch e {
case nil:
default:
Expand Down Expand Up @@ -1575,7 +1583,6 @@ func (t *tScreen) HasKey(k Key) bool {

func (t *tScreen) Resize(int, int, int, int) {}


func (t *tScreen) Suspend() error {
t.disengage()
return nil
Expand All @@ -1584,3 +1591,82 @@ func (t *tScreen) Suspend() error {
func (t *tScreen) Resume() error {
return t.engage()
}

// engage is used to place the terminal in raw mode and establish screen size, etc.
// Thing of this is as tcell "engaging" the clutch, as it's going to be driving the
// terminal interface.
func (t *tScreen) engage() error {
t.Lock()
defer t.Unlock()
if t.tty == nil {
return ErrNoScreen
}
if t.stopQ != nil {
return errors.New("already engaged")
}
if err := t.tty.Start(); err != nil {
return err
}
if w, h, err := t.tty.WindowSize(); err == nil && w != 0 && h != 0 {
t.cells.Resize(w, h)
}
stopQ := make(chan struct{})
t.stopQ = stopQ
t.enableMouse(t.mouseFlags)
t.enablePasting(t.pasteEnabled)

ti := t.ti
t.TPuts(ti.EnterCA)
t.TPuts(ti.EnterKeypad)
t.TPuts(ti.HideCursor)
t.TPuts(ti.EnableAcs)
t.TPuts(ti.Clear)

t.wg.Add(2)
go t.inputLoop(stopQ)
go t.mainLoop(stopQ)
return nil
}

// disengage is used to release the terminal back to support from the caller.
// Think of this as tcell disengaging the clutch, so that another application
// can take over the terminal interface. This restores the TTY mode that was
// present when the application was first started.
func (t *tScreen) disengage() {

t.Lock()
stopQ := t.stopQ
t.stopQ = nil
close(stopQ)
_ = t.tty.Drain()
t.Unlock()

// wait for everything to shut down
t.wg.Wait()

// shutdown the screen and disable special modes (e.g. mouse and bracketed paste)
ti := t.ti
t.cells.Resize(0, 0)
t.TPuts(ti.ShowCursor)
t.TPuts(ti.AttrOff)
t.TPuts(ti.Clear)
t.TPuts(ti.ExitCA)
t.TPuts(ti.ExitKeypad)
t.enableMouse(0)
t.enablePasting(false)

_ = t.tty.Stop()
}

// Beep emits a beep to the terminal.
func (t *tScreen) Beep() error {
t.writeString(string(byte(7)))
return nil
}

// finalize is used to at application shutdown, and restores the terminal
// to it's initial state. It should not be called more than once.
func (t *tScreen) finalize() {
t.disengage()
_ = t.tty.Close()
}
18 changes: 0 additions & 18 deletions tscreen_stub.go
Expand Up @@ -20,24 +20,6 @@ package tcell
// that would probably mean sacrificing some of the richer key reporting
// that we can obtain with the console API present on Windows.

func (t *tScreen) engage() error {
return ErrNoScreen
}

func (t *tScreen) disengage() {
}

func (t *tScreen) initialize() error {
return ErrNoScreen
}

func (t *tScreen) finalize() {
}

func (t *tScreen) getWinSize() (int, int, error) {
return 0, 0, ErrNoScreen
}

func (t *tScreen) Beep() error {
return ErrNoScreen
}

0 comments on commit 97c0480

Please sign in to comment.