123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386 |
- // License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
- package tty
- import (
- "encoding/base64"
- "errors"
- "fmt"
- "io"
- "os"
- "strconv"
- "sync"
- "time"
- "golang.org/x/sys/unix"
- "kitty/tools/utils"
- )
- const (
- TCSANOW = 0
- TCSADRAIN = 1
- TCSAFLUSH = 2
- )
- type Term struct {
- os_file *os.File
- states []unix.Termios
- }
- func eintr_retry_noret(f func() error) error {
- for {
- qerr := f()
- if qerr == unix.EINTR {
- continue
- }
- return qerr
- }
- }
- func eintr_retry_intret(f func() (int, error)) (int, error) {
- for {
- q, qerr := f()
- if qerr == unix.EINTR {
- continue
- }
- return q, qerr
- }
- }
- func IsTerminal(fd uintptr) bool {
- var t unix.Termios
- err := eintr_retry_noret(func() error { return Tcgetattr(int(fd), &t) })
- return err == nil
- }
- type TermiosOperation func(t *unix.Termios)
- func get_vmin_and_vtime(d time.Duration) (uint8, uint8) {
- if d > 0 {
- // VTIME is expressed in terms of deciseconds
- vtimeDeci := d.Milliseconds() / 100
- // ensure valid range
- vtime := uint8(clamp(vtimeDeci, 1, 0xff))
- return 0, vtime
- }
- // block indefinitely until we receive at least 1 byte
- return 1, 0
- }
- func SetReadTimeout(d time.Duration) TermiosOperation {
- vmin, vtime := get_vmin_and_vtime(d)
- return func(t *unix.Termios) {
- t.Cc[unix.VMIN] = vmin
- t.Cc[unix.VTIME] = vtime
- }
- }
- var SetBlockingRead TermiosOperation = SetReadTimeout(0)
- var SetRaw TermiosOperation = func(t *unix.Termios) {
- // This attempts to replicate the behaviour documented for cfmakeraw in
- // the termios(3) manpage, as Go doesn't wrap cfmakeraw probably because its not in POSIX
- t.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON
- t.Oflag &^= unix.OPOST
- t.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN
- t.Cflag &^= unix.CSIZE | unix.PARENB
- t.Cflag |= unix.CS8
- t.Cc[unix.VMIN] = 1
- t.Cc[unix.VTIME] = 0
- }
- var SetNoEcho TermiosOperation = func(t *unix.Termios) {
- t.Lflag &^= unix.ECHO
- }
- var SetReadPassword TermiosOperation = func(t *unix.Termios) {
- t.Lflag &^= unix.ECHO
- t.Lflag |= unix.ISIG
- t.Lflag &^= unix.ICANON
- t.Iflag |= unix.ICRNL
- t.Cc[unix.VMIN] = 1
- t.Cc[unix.VTIME] = 0
- }
- func WrapTerm(fd int, name string, operations ...TermiosOperation) (self *Term, err error) {
- if name == "" {
- name = fmt.Sprintf("<fd: %d>", fd)
- }
- os_file := os.NewFile(uintptr(fd), name)
- if os_file == nil {
- return nil, os.ErrInvalid
- }
- self = &Term{os_file: os_file}
- err = self.ApplyOperations(TCSANOW, operations...)
- if err != nil {
- self.Close()
- self = nil
- }
- return
- }
- func OpenTerm(name string, operations ...TermiosOperation) (self *Term, err error) {
- fd, err := eintr_retry_intret(func() (int, error) {
- return unix.Open(name, unix.O_NOCTTY|unix.O_CLOEXEC|unix.O_NDELAY|unix.O_RDWR, 0666)
- })
- if err != nil {
- return nil, &os.PathError{Op: "open", Path: name, Err: err}
- }
- self, err = WrapTerm(fd, name, operations...)
- return
- }
- func OpenControllingTerm(operations ...TermiosOperation) (self *Term, err error) {
- return OpenTerm(Ctermid(), operations...)
- }
- func (self *Term) Fd() int {
- if self.os_file == nil {
- return -1
- }
- return int(self.os_file.Fd())
- }
- func (self *Term) Close() error {
- if self.os_file == nil {
- return nil
- }
- err := eintr_retry_noret(func() error { return self.os_file.Close() })
- self.os_file = nil
- return err
- }
- func (self *Term) WasEchoOnOriginally() bool {
- if len(self.states) > 0 {
- return self.states[0].Lflag&unix.ECHO != 0
- }
- return false
- }
- func (self *Term) Tcgetattr(ans *unix.Termios) error {
- return eintr_retry_noret(func() error { return Tcgetattr(self.Fd(), ans) })
- }
- func (self *Term) Tcsetattr(when uintptr, ans *unix.Termios) error {
- return eintr_retry_noret(func() error { return Tcsetattr(self.Fd(), when, ans) })
- }
- func (self *Term) set_termios_attrs(when uintptr, modify func(*unix.Termios)) (err error) {
- var state unix.Termios
- if err = self.Tcgetattr(&state); err != nil {
- return
- }
- new_state := state
- modify(&new_state)
- if err = self.Tcsetattr(when, &new_state); err == nil {
- self.states = append(self.states, state)
- }
- return
- }
- func (self *Term) ApplyOperations(when uintptr, operations ...TermiosOperation) (err error) {
- if len(operations) == 0 {
- return
- }
- return self.set_termios_attrs(when, func(t *unix.Termios) {
- for _, op := range operations {
- op(t)
- }
- })
- }
- func (self *Term) PopStateWhen(when uintptr) (err error) {
- if len(self.states) == 0 {
- return nil
- }
- idx := len(self.states) - 1
- if err = self.Tcsetattr(when, &self.states[idx]); err == nil {
- self.states = self.states[:idx]
- }
- return
- }
- func (self *Term) PopState() error {
- return self.PopStateWhen(TCSAFLUSH)
- }
- func (self *Term) RestoreWhen(when uintptr) (err error) {
- if len(self.states) == 0 {
- return nil
- }
- self.states = self.states[:1]
- return self.PopStateWhen(when)
- }
- func (self *Term) Restore() error {
- return self.RestoreWhen(TCSAFLUSH)
- }
- func (self *Term) RestoreAndClose() error {
- _ = self.Restore()
- return self.Close()
- }
- func (self *Term) Suspend() (resume func() error, err error) {
- var state unix.Termios
- err = self.Tcgetattr(&state)
- if err != nil {
- return nil, err
- }
- if len(self.states) > 0 {
- err := self.Tcsetattr(TCSANOW, &self.states[0])
- if err != nil {
- return nil, err
- }
- }
- return func() error { return self.Tcsetattr(TCSANOW, &state) }, nil
- }
- func (self *Term) SuspendAndRun(callback func() error) error {
- resume, err := self.Suspend()
- if err != nil {
- return err
- }
- err = callback()
- if rerr := resume(); rerr != nil {
- err = rerr
- }
- return err
- }
- func clamp(v, lo, hi int64) int64 {
- if v < lo {
- return lo
- }
- if v > hi {
- return hi
- }
- return v
- }
- func (self *Term) ReadWithTimeout(b []byte, d time.Duration) (n int, err error) {
- var read, write, in_err unix.FdSet
- pselect := func() (int, error) {
- read.Zero()
- write.Zero()
- in_err.Zero()
- read.Set(self.Fd())
- return utils.Select(self.Fd()+1, &read, &write, &in_err, d)
- }
- num_ready, err := pselect()
- if err != nil {
- return 0, err
- }
- if num_ready == 0 {
- err = os.ErrDeadlineExceeded
- return 0, err
- }
- for {
- n, err = self.Read(b)
- if errors.Is(err, unix.EINTR) {
- continue
- }
- return n, err
- }
- }
- func is_temporary_read_error(err error) bool {
- return errors.Is(err, unix.EINTR) || errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EWOULDBLOCK)
- }
- func (self *Term) Read(b []byte) (n int, err error) {
- for {
- n, err = self.os_file.Read(b)
- // On macOS we get EAGAIN if another thread is writing to the tty at the same time
- if err != nil && is_temporary_read_error(err) && n <= 0 {
- continue
- }
- return
- }
- }
- func (self *Term) Write(b []byte) (int, error) {
- return self.os_file.Write(b)
- }
- func is_temporary_error(err error) bool {
- return errors.Is(err, unix.EINTR) || errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EWOULDBLOCK) || errors.Is(err, io.ErrShortWrite)
- }
- func (self *Term) WriteAll(b []byte) error {
- for len(b) > 0 {
- n, err := self.os_file.Write(b)
- if err != nil && !is_temporary_error(err) {
- return err
- }
- b = b[n:]
- }
- return nil
- }
- func (self *Term) WriteAllString(s string) error {
- return self.WriteAll(utils.UnsafeStringToBytes(s))
- }
- func (self *Term) WriteString(b string) (int, error) {
- return self.os_file.WriteString(b)
- }
- func (self *Term) DebugPrintln(a ...any) {
- msg := []byte(fmt.Sprintln(a...))
- const limit = 2048
- encoded := make([]byte, limit*2)
- for i := 0; i < len(msg); i += limit {
- end := i + limit
- if end > len(msg) {
- end = len(msg)
- }
- chunk := msg[i:end]
- encoded = encoded[:cap(encoded)]
- base64.StdEncoding.Encode(encoded, chunk)
- _, _ = self.WriteString("\x1bP@kitty-print|")
- _, _ = self.Write(encoded)
- _, _ = self.WriteString("\x1b\\")
- }
- }
- func GetSize(fd int) (*unix.Winsize, error) {
- for {
- sz, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ)
- if err != unix.EINTR {
- return sz, err
- }
- }
- }
- func (self *Term) GetSize() (*unix.Winsize, error) {
- return GetSize(self.Fd())
- }
- // go doesn't have a wrapper for ctermid()
- func Ctermid() string { return "/dev/tty" }
- var KittyStdout = sync.OnceValue(func() *os.File {
- if fds := os.Getenv(`KITTY_STDIO_FORWARDED`); fds != "" {
- if fd, err := strconv.Atoi(fds); err == nil && fd > -1 {
- if f := os.NewFile(uintptr(fd), "<kitty_stdout>"); f != nil {
- return f
- }
- }
- }
- return nil
- })
- func DebugPrintln(a ...any) {
- if f := KittyStdout(); f != nil {
- fmt.Fprintln(f, a...)
- return
- }
- term, err := OpenControllingTerm()
- if err == nil {
- defer term.Close()
- term.DebugPrintln(a...)
- }
- }
|