mirror of
https://github.com/gdamore/tcell.git
synced 2025-04-26 13:48:53 +08:00
fixes #452 Lost a key event once when exiting or suspending in v2.2.1.
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.
This commit is contained in:
parent
8f925d8272
commit
97c0480839
@ -17,7 +17,6 @@
|
||||
package tcell
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
@ -25,26 +24,18 @@ import (
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
// Copyright 2021 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build plan9 windows js
|
||||
|
||||
package tcell
|
||||
|
||||
func (t *tScreen) nonBlocking(on bool) error {
|
||||
return nil
|
||||
}
|
@ -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
tscreen.go
104
tscreen.go
@ -16,6 +16,7 @@ package tcell
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
@ -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"))
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -731,7 +739,7 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -739,7 +747,7 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
@ -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
|
||||
@ -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:
|
||||
@ -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
|
||||
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
112
tscreen_unix.go
112
tscreen_unix.go
@ -16,118 +16,16 @@
|
||||
|
||||
package tcell
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// 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.stopQ != nil {
|
||||
return errors.New("already engaged")
|
||||
}
|
||||
if _, err := term.MakeRaw(int(t.in.Fd())); err != nil {
|
||||
return err
|
||||
}
|
||||
if w, h, err := term.GetSize(int(t.in.Fd())); err == nil && w != 0 && h != 0 {
|
||||
t.cells.Resize(w, h)
|
||||
}
|
||||
stopQ := make(chan struct{})
|
||||
t.stopQ = stopQ
|
||||
t.nonBlocking(false)
|
||||
t.enableMouse(t.mouseFlags)
|
||||
t.enablePasting(t.pasteEnabled)
|
||||
signal.Notify(t.sigwinch, syscall.SIGWINCH)
|
||||
|
||||
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()
|
||||
t.nonBlocking(true)
|
||||
stopQ := t.stopQ
|
||||
t.stopQ = nil
|
||||
close(stopQ)
|
||||
t.Unlock()
|
||||
|
||||
// wait for everything to shut down
|
||||
t.wg.Wait()
|
||||
|
||||
signal.Stop(t.sigwinch)
|
||||
|
||||
// put back normal blocking mode
|
||||
t.nonBlocking(false)
|
||||
|
||||
// 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)
|
||||
|
||||
// restore the termios that we were started with
|
||||
_ = term.Restore(int(t.in.Fd()), t.saved)
|
||||
|
||||
}
|
||||
|
||||
// initialize is used at application startup, and sets up the initial values
|
||||
// including file descriptors used for terminals and saving the initial state
|
||||
// so that it can be restored when the application terminates.
|
||||
func (t *tScreen) initialize() error {
|
||||
var err error
|
||||
t.out = os.Stdout
|
||||
if t.in, err = os.Open("/dev/tty"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.saved, err = term.GetState(int(t.in.Fd()))
|
||||
if err == nil {
|
||||
return nil
|
||||
if t.tty == nil {
|
||||
t.tty, err = NewDevTty()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
// getWinSize is called to obtain the terminal dimensions.
|
||||
func (t *tScreen) getWinSize() (int, int, error) {
|
||||
return term.GetSize(int(t.in.Fd()))
|
||||
}
|
||||
|
||||
// Beep emits a beep to the terminal.
|
||||
func (t *tScreen) Beep() error {
|
||||
t.writeString(string(byte(7)))
|
||||
return nil
|
||||
}
|
||||
|
56
tty.go
Normal file
56
tty.go
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright 2021 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
import "io"
|
||||
|
||||
// Tty is an abstraction of a tty (traditionally "teletype"). This allows applications to
|
||||
// provide for alternate backends, as there are situations where the traditional /dev/tty
|
||||
// does not work, or where more flexible handling is required. This interface is for use
|
||||
// with the terminfo-style based API. It extends the io.ReadWriter API. It is reasonable
|
||||
// that the implementation might choose to use different underlying files for the Reader
|
||||
// and Writer sides of this API, as part of it's internal implementation.
|
||||
type Tty interface {
|
||||
// Start is used to activate the Tty for use. Upon return the terminal should be
|
||||
// in raw mode, non-blocking, etc. The implementation should take care of saving
|
||||
// any state that is required so that it may be restored when Stop is called.
|
||||
Start() error
|
||||
|
||||
// Stop is used to stop using this Tty instance. This may be a suspend, so that other
|
||||
// terminal based applications can run in the foreground. Implementations should
|
||||
// restore any state collected at Start(), and return to ordinary blocking mode, etc.
|
||||
// Drain is called first to drain the input. Once this is called, no more Read
|
||||
// or Write calls will be made until Start is called again.
|
||||
Stop() error
|
||||
|
||||
// Drain is called before Stop, and ensures that the reader will wake up appropriately
|
||||
// if it was blocked. This workaround is required for /dev/tty on certain UNIX systems
|
||||
// to ensure that Read() does not block forever. This typically arranges for the tty driver
|
||||
// to send data immediately (e.g. VMIN and VTIME both set zero) and sets a deadline on input.
|
||||
// Implementations may reasonably make this a no-op. There will still be control sequences
|
||||
// emitted between the time this is called, and when Stop is called.
|
||||
Drain() error
|
||||
|
||||
// NotifyResize is used register a callback when the tty thinks the dimensions have
|
||||
// changed. The standard UNIX implementation links this to a handler for SIGWINCH.
|
||||
// If the supplied callback is nil, then any handler should be unregistered.
|
||||
NotifyResize(cb func())
|
||||
|
||||
// WindowSize is called to determine the terminal dimensions. This might be determined
|
||||
// by an ioctl or other means.
|
||||
WindowSize() (width int, height int, err error)
|
||||
|
||||
io.ReadWriteCloser
|
||||
}
|
163
tty_unix.go
Normal file
163
tty_unix.go
Normal file
@ -0,0 +1,163 @@
|
||||
// Copyright 2021 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos
|
||||
|
||||
package tcell
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// devTty is an implementation of the Tty API based upon /dev/tty.
|
||||
type devTty struct {
|
||||
fd int
|
||||
f *os.File
|
||||
of *os.File // the first open of /dev/tty
|
||||
saved *term.State
|
||||
sig chan os.Signal
|
||||
cb func()
|
||||
stopQ chan struct{}
|
||||
wg sync.WaitGroup
|
||||
l sync.Mutex
|
||||
}
|
||||
|
||||
func (tty *devTty) Read(b []byte) (int, error) {
|
||||
return tty.f.Read(b)
|
||||
}
|
||||
|
||||
func (tty *devTty) Write(b []byte) (int, error) {
|
||||
return tty.f.Write(b)
|
||||
}
|
||||
|
||||
func (tty *devTty) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tty *devTty) Start() error {
|
||||
tty.l.Lock()
|
||||
defer tty.l.Unlock()
|
||||
|
||||
// We open another copy of /dev/tty. This is a workaround for unusual behavior
|
||||
// observed in macOS, apparently caused when a subshell (for example) closes our
|
||||
// own tty device (when it exits for example). Getting a fresh new one seems to
|
||||
// resolve the problem. (We believe this is a bug in the macOS tty driver that
|
||||
// fails to account for dup() references to the same file before applying close()
|
||||
// related behaviors to the tty.) We're also holding the original copy we opened
|
||||
// since closing that might have deleterious effects as well. The upshot is that
|
||||
// we will have up to two separate file handles open on /dev/tty. (Note that when
|
||||
// using stdin/stdout instead of /dev/tty this problem is not observed.)
|
||||
var err error
|
||||
if tty.f, err = os.OpenFile("/dev/tty", os.O_RDWR, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
tty.fd = int(tty.of.Fd())
|
||||
|
||||
if !term.IsTerminal(tty.fd) {
|
||||
return errors.New("device is not a terminal")
|
||||
}
|
||||
|
||||
_ = tty.f.SetReadDeadline(time.Time{})
|
||||
saved, err := term.MakeRaw(tty.fd) // also sets vMin and vTime
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tty.saved = saved
|
||||
|
||||
tty.stopQ = make(chan struct{})
|
||||
tty.wg.Add(1)
|
||||
go func(stopQ chan struct{}) {
|
||||
defer tty.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-tty.sig:
|
||||
tty.l.Lock()
|
||||
cb := tty.cb
|
||||
tty.l.Unlock()
|
||||
cb()
|
||||
case <-stopQ:
|
||||
return
|
||||
}
|
||||
}
|
||||
}(tty.stopQ)
|
||||
|
||||
signal.Notify(tty.sig, syscall.SIGWINCH)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tty *devTty) Drain() error {
|
||||
_ = tty.f.SetReadDeadline(time.Now())
|
||||
if err := tcSetBufParams(tty.fd, 0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tty *devTty) Stop() error {
|
||||
tty.l.Lock()
|
||||
if err := term.Restore(tty.fd, tty.saved); err != nil {
|
||||
tty.l.Unlock()
|
||||
return err
|
||||
}
|
||||
_ = tty.f.SetReadDeadline(time.Now())
|
||||
|
||||
signal.Stop(tty.sig)
|
||||
close(tty.stopQ)
|
||||
tty.l.Unlock()
|
||||
|
||||
tty.wg.Wait()
|
||||
|
||||
// close our tty device -- we'll get another one if we Start again later.
|
||||
_ = tty.f.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tty *devTty) WindowSize() (int, int, error) {
|
||||
return term.GetSize(tty.fd)
|
||||
}
|
||||
|
||||
func (tty *devTty) NotifyResize(cb func()) {
|
||||
tty.l.Lock()
|
||||
tty.cb = cb
|
||||
tty.l.Unlock()
|
||||
}
|
||||
|
||||
func NewDevTty() (Tty, error) {
|
||||
tty := &devTty{
|
||||
sig: make(chan os.Signal),
|
||||
}
|
||||
var err error
|
||||
if tty.of, err = os.OpenFile("/dev/tty", os.O_RDWR, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tty.fd = int(tty.of.Fd())
|
||||
if !term.IsTerminal(tty.fd) {
|
||||
_ = tty.f.Close()
|
||||
return nil, errors.New("not a terminal")
|
||||
}
|
||||
if tty.saved, err = term.GetState(tty.fd); err != nil {
|
||||
_ = tty.f.Close()
|
||||
return nil, fmt.Errorf("failed to get state: %w", err)
|
||||
}
|
||||
return tty, nil
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user