stdin_unix.go (4337B)
1 // Copyright 2021 The TCell Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use file except in compliance with the License. 5 // You may obtain a copy of the license at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos 16 // +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos 17 18 package tcell 19 20 import ( 21 "errors" 22 "fmt" 23 "os" 24 "os/signal" 25 "strconv" 26 "sync" 27 "syscall" 28 "time" 29 30 "golang.org/x/sys/unix" 31 "golang.org/x/term" 32 ) 33 34 // stdIoTty is an implementation of the Tty API based upon stdin/stdout. 35 type stdIoTty struct { 36 fd int 37 in *os.File 38 out *os.File 39 saved *term.State 40 sig chan os.Signal 41 cb func() 42 stopQ chan struct{} 43 dev string 44 wg sync.WaitGroup 45 l sync.Mutex 46 } 47 48 func (tty *stdIoTty) Read(b []byte) (int, error) { 49 return tty.in.Read(b) 50 } 51 52 func (tty *stdIoTty) Write(b []byte) (int, error) { 53 return tty.out.Write(b) 54 } 55 56 func (tty *stdIoTty) Close() error { 57 return nil 58 } 59 60 func (tty *stdIoTty) Start() error { 61 tty.l.Lock() 62 defer tty.l.Unlock() 63 64 // We open another copy of /dev/tty. This is a workaround for unusual behavior 65 // observed in macOS, apparently caused when a sub-shell (for example) closes our 66 // own tty device (when it exits for example). Getting a fresh new one seems to 67 // resolve the problem. (We believe this is a bug in the macOS tty driver that 68 // fails to account for dup() references to the same file before applying close() 69 // related behaviors to the tty.) We're also holding the original copy we opened 70 // since closing that might have deleterious effects as well. The upshot is that 71 // we will have up to two separate file handles open on /dev/tty. (Note that when 72 // using stdin/stdout instead of /dev/tty this problem is not observed.) 73 var err error 74 tty.in = os.Stdin 75 tty.out = os.Stdout 76 tty.fd = int(tty.in.Fd()) 77 78 if !term.IsTerminal(tty.fd) { 79 return errors.New("device is not a terminal") 80 } 81 82 _ = tty.in.SetReadDeadline(time.Time{}) 83 saved, err := term.MakeRaw(tty.fd) // also sets vMin and vTime 84 if err != nil { 85 return err 86 } 87 tty.saved = saved 88 89 tty.stopQ = make(chan struct{}) 90 tty.wg.Add(1) 91 go func(stopQ chan struct{}) { 92 defer tty.wg.Done() 93 for { 94 select { 95 case <-tty.sig: 96 tty.l.Lock() 97 cb := tty.cb 98 tty.l.Unlock() 99 if cb != nil { 100 cb() 101 } 102 case <-stopQ: 103 return 104 } 105 } 106 }(tty.stopQ) 107 108 signal.Notify(tty.sig, syscall.SIGWINCH) 109 return nil 110 } 111 112 func (tty *stdIoTty) Drain() error { 113 _ = tty.in.SetReadDeadline(time.Now()) 114 if err := tcSetBufParams(tty.fd, 0, 0); err != nil { 115 return err 116 } 117 return nil 118 } 119 120 func (tty *stdIoTty) Stop() error { 121 tty.l.Lock() 122 if err := term.Restore(tty.fd, tty.saved); err != nil { 123 tty.l.Unlock() 124 return err 125 } 126 _ = tty.in.SetReadDeadline(time.Now()) 127 128 signal.Stop(tty.sig) 129 close(tty.stopQ) 130 tty.l.Unlock() 131 132 tty.wg.Wait() 133 134 return nil 135 } 136 137 func (tty *stdIoTty) WindowSize() (WindowSize, error) { 138 size := WindowSize{} 139 ws, err := unix.IoctlGetWinsize(tty.fd, unix.TIOCGWINSZ) 140 if err != nil { 141 return size, err 142 } 143 w := int(ws.Col) 144 h := int(ws.Row) 145 if w == 0 { 146 w, _ = strconv.Atoi(os.Getenv("COLUMNS")) 147 } 148 if w == 0 { 149 w = 80 // default 150 } 151 if h == 0 { 152 h, _ = strconv.Atoi(os.Getenv("LINES")) 153 } 154 if h == 0 { 155 h = 25 // default 156 } 157 size.Width = w 158 size.Height = h 159 size.PixelWidth = int(ws.Xpixel) 160 size.PixelHeight = int(ws.Ypixel) 161 return size, nil 162 } 163 164 func (tty *stdIoTty) NotifyResize(cb func()) { 165 tty.l.Lock() 166 tty.cb = cb 167 tty.l.Unlock() 168 } 169 170 // NewStdioTty opens a tty using standard input/output. 171 func NewStdIoTty() (Tty, error) { 172 tty := &stdIoTty{ 173 sig: make(chan os.Signal), 174 in: os.Stdin, 175 out: os.Stdout, 176 } 177 var err error 178 tty.fd = int(tty.in.Fd()) 179 if !term.IsTerminal(tty.fd) { 180 return nil, errors.New("not a terminal") 181 } 182 if tty.saved, err = term.GetState(tty.fd); err != nil { 183 return nil, fmt.Errorf("failed to get state: %w", err) 184 } 185 return tty, nil 186 }