Files
kitty/tools/tui/loop/read.go
Kovid Goyal 5e158f90a7 Fix some responses from terminal sometimes leaking into shell on after kitten exit
Always do a roundtrip at kitten exit, except for special purpose
kittens. This slows down exit by one round trip time (capped at 2
seconds), however it ensures that we never get terminal response leak.

Fixes #9839
2026-04-10 15:14:38 +05:30

157 lines
3.0 KiB
Go

// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package loop
import (
"fmt"
"io"
"os"
"regexp"
"strings"
"time"
"golang.org/x/sys/unix"
"github.com/kovidgoyal/go-parallel"
"github.com/kovidgoyal/kitty/tools/tty"
"github.com/kovidgoyal/kitty/tools/utils"
)
var _ = fmt.Print
func (self *Loop) dispatch_input_data(data []byte) error {
if self.OnReceivedData != nil {
err := self.OnReceivedData(data)
if err != nil {
return err
}
}
err := self.escape_code_parser.Parse(data)
if err != nil {
return err
}
return nil
}
func read_ignoring_temporary_errors(f *tty.Term, buf []byte) (int, error) {
n, err := f.Read(buf)
if is_temporary_error(err) {
return 0, nil
}
if n == 0 {
return 0, io.EOF
}
return n, err
}
func read_from_tty(pipe_r *os.File, term *tty.Term, results_channel chan<- []byte, err_channel chan<- error, quit_channel <-chan byte, leftover_channel chan<- []byte) {
defer func() {
if r := recover(); r != nil {
err := parallel.Format_stacktrace_on_panic(r, 1)
err_channel <- err
}
}()
keep_going := true
pipe_fd := int(pipe_r.Fd())
tty_fd := term.Fd()
selector := utils.CreateSelect(2)
selector.RegisterRead(pipe_fd)
selector.RegisterRead(tty_fd)
defer func() {
close(results_channel)
pipe_r.Close()
}()
const bufsize = 2 * utils.DEFAULT_IO_BUFFER_SIZE
wait_for_read_available := func() {
for {
n, err := selector.WaitForever()
if err != nil && err != unix.EINTR {
err_channel <- err
keep_going = false
return
}
if n > 0 {
break
}
}
if selector.IsReadyToRead(pipe_fd) {
keep_going = false
}
}
buf := make([]byte, bufsize)
for keep_going {
if len(buf) < 64 {
buf = make([]byte, bufsize)
}
if wait_for_read_available(); !keep_going {
break
}
n, err := read_ignoring_temporary_errors(term, buf)
if err != nil {
err_channel <- err
keep_going = false
break
}
if n == 0 { // temporary error
continue
}
send := buf[:n]
buf = buf[n:]
select {
case results_channel <- send:
case <-quit_channel:
leftover_channel <- send
keep_going = false
}
}
}
func has_da1_response(s string) bool {
pat := regexp.MustCompile("\x1b\\[\\?[0-9:;]+c")
return pat.FindString(s) != ""
}
func do_roundtrip_to_terminal(term *tty.Term, timeout time.Duration) {
// ask for primary device attributes
for {
if err := term.WriteAllString("\x1b[c"); err != nil && !is_temporary_error(err) {
return
} else {
break
}
}
s := strings.Builder{}
s.Grow(256)
received := make(chan error)
go func() {
defer func() {
if r := recover(); r != nil {
text := parallel.Format_stacktrace_on_panic(r, 1).Error()
received <- fmt.Errorf("%s", text)
}
}()
for {
buf := make([]byte, 1024)
n, err := read_ignoring_temporary_errors(term, buf)
if n > 0 {
s.Write(buf[:n])
if has_da1_response(s.String()) {
received <- nil
return
}
}
if err != nil {
received <- err
}
}
}()
select {
case <-received:
case <-time.After(timeout):
}
}