152 lines
2.7 KiB
Go
152 lines
2.7 KiB
Go
//go:build unix
|
|
|
|
package terminal
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
"golang.org/x/sys/unix"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
type unixBackend struct {
|
|
in *os.File
|
|
out *os.File
|
|
inFd int
|
|
outFd int
|
|
oldTerm *term.State
|
|
|
|
resizeStopCh chan struct{}
|
|
resizeDoneCh chan struct{}
|
|
}
|
|
|
|
const escapeTimeoutMs = 10
|
|
|
|
func newBackend() Backend {
|
|
return &unixBackend{
|
|
in: os.Stdin,
|
|
out: os.Stdout,
|
|
inFd: int(os.Stdin.Fd()),
|
|
outFd: int(os.Stdout.Fd()),
|
|
}
|
|
}
|
|
|
|
func (b *unixBackend) Init() error {
|
|
if !term.IsTerminal(b.inFd) {
|
|
return fmt.Errorf("stdin is not a terminal")
|
|
}
|
|
|
|
old, err := term.MakeRaw(b.inFd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.oldTerm = old
|
|
return nil
|
|
}
|
|
|
|
func (b *unixBackend) Fini() {
|
|
if b.resizeStopCh != nil {
|
|
close(b.resizeStopCh)
|
|
<-b.resizeDoneCh
|
|
b.resizeStopCh = nil
|
|
}
|
|
if b.oldTerm != nil {
|
|
term.Restore(b.inFd, b.oldTerm)
|
|
}
|
|
}
|
|
|
|
func (b *unixBackend) Size() (int, int) {
|
|
return getTerminalSize(b.outFd)
|
|
}
|
|
|
|
func (b *unixBackend) Write(p []byte) error {
|
|
_, err := b.out.Write(p)
|
|
return err
|
|
}
|
|
|
|
// Read implements the polling logic previously in input.go
|
|
func (b *unixBackend) Read(stopCh <-chan struct{}) ([]byte, error) {
|
|
// Buffer for single read
|
|
buf := make([]byte, 256)
|
|
|
|
for {
|
|
select {
|
|
case <-stopCh:
|
|
return nil, nil
|
|
default:
|
|
}
|
|
|
|
// Poll with timeout to allow checking stopCh
|
|
fds := []unix.PollFd{
|
|
{Fd: int32(b.inFd), Events: unix.POLLIN},
|
|
}
|
|
|
|
// Timeout to differentiate single space and space sequences
|
|
n, err := unix.Poll(fds, escapeTimeoutMs)
|
|
if err != nil {
|
|
if errors.Is(err, unix.EINTR) {
|
|
continue
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if n == 0 {
|
|
// Timeout - return empty to let readLoop handle pending ESC
|
|
return nil, nil
|
|
}
|
|
|
|
// Read data
|
|
rn, err := unix.Read(b.inFd, buf)
|
|
if err != nil {
|
|
if errors.Is(err, unix.EINTR) || errors.Is(err, unix.EAGAIN) {
|
|
continue
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if rn == 0 {
|
|
// EOF
|
|
return nil, nil
|
|
}
|
|
|
|
// Return copy of data
|
|
ret := make([]byte, rn)
|
|
copy(ret, buf[:rn])
|
|
return ret, nil
|
|
}
|
|
}
|
|
|
|
func (b *unixBackend) SetResizeHandler(handler func(width, height int)) {
|
|
b.resizeStopCh = make(chan struct{})
|
|
b.resizeDoneCh = make(chan struct{})
|
|
|
|
go func() {
|
|
defer close(b.resizeDoneCh)
|
|
sigCh := make(chan os.Signal, 1)
|
|
signal.Notify(sigCh, syscall.SIGWINCH)
|
|
defer signal.Stop(sigCh)
|
|
|
|
for {
|
|
select {
|
|
case <-b.resizeStopCh:
|
|
return
|
|
case <-sigCh:
|
|
w, h := b.Size()
|
|
handler(w, h)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// getTerminalSize returns the terminal size for a given fd
|
|
func getTerminalSize(fd int) (int, int) {
|
|
ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ)
|
|
if err != nil {
|
|
return 80, 24 // Fallback
|
|
}
|
|
return int(ws.Col), int(ws.Row)
|
|
} |