158 lines
2.9 KiB
Go
158 lines
2.9 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)
|
|
}
|
|
}
|
|
|
|
// Size delegates to exported WindowSize; getTerminalSize deleted
|
|
func (b *unixBackend) Size() (int, int) {
|
|
if w, h, ok := WindowSize(b.out); ok {
|
|
return w, h
|
|
}
|
|
return 80, 24 // Fallback
|
|
}
|
|
|
|
// WindowSize queries terminal dimensions for f without raw mode or
|
|
// Terminal lifecycle. ok=false when f is not a terminal.
|
|
func WindowSize(f *os.File) (w, h int, ok bool) {
|
|
ws, err := unix.IoctlGetWinsize(int(f.Fd()), unix.TIOCGWINSZ)
|
|
if err != nil || ws.Col == 0 {
|
|
return 0, 0, false
|
|
}
|
|
return int(ws.Col), int(ws.Row), true
|
|
}
|
|
|
|
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 standalone ESC from escape 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)
|
|
}
|
|
}
|
|
}()
|
|
}
|