nt

A sensible note-taking program
git clone git://git.laack.co/nt.git
Log | Files | Refs | README

tty_unix.go (4724B)


      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 // devTty is an implementation of the Tty API based upon /dev/tty.
     35 type devTty struct {
     36 	fd    int
     37 	f     *os.File
     38 	of    *os.File // the first open of /dev/tty
     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 *devTty) Read(b []byte) (int, error) {
     49 	return tty.f.Read(b)
     50 }
     51 
     52 func (tty *devTty) Write(b []byte) (int, error) {
     53 	return tty.f.Write(b)
     54 }
     55 
     56 func (tty *devTty) Close() error {
     57 	return tty.f.Close()
     58 }
     59 
     60 func (tty *devTty) 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 subshell (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 	if tty.f, err = os.OpenFile(tty.dev, os.O_RDWR, 0); err != nil {
     75 		return err
     76 	}
     77 
     78 	if !term.IsTerminal(tty.fd) {
     79 		return errors.New("device is not a terminal")
     80 	}
     81 
     82 	_ = tty.f.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 *devTty) Drain() error {
    113 	_ = tty.f.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 *devTty) 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.f.SetReadDeadline(time.Now())
    127 
    128 	signal.Stop(tty.sig)
    129 	close(tty.stopQ)
    130 	tty.l.Unlock()
    131 
    132 	tty.wg.Wait()
    133 
    134 	// close our tty device -- we'll get another one if we Start again later.
    135 	_ = tty.f.Close()
    136 
    137 	return nil
    138 }
    139 
    140 func (tty *devTty) WindowSize() (WindowSize, error) {
    141 	size := WindowSize{}
    142 	ws, err := unix.IoctlGetWinsize(tty.fd, unix.TIOCGWINSZ)
    143 	if err != nil {
    144 		return size, err
    145 	}
    146 	w := int(ws.Col)
    147 	h := int(ws.Row)
    148 	if w == 0 {
    149 		w, _ = strconv.Atoi(os.Getenv("COLUMNS"))
    150 	}
    151 	if w == 0 {
    152 		w = 80 // default
    153 	}
    154 	if h == 0 {
    155 		h, _ = strconv.Atoi(os.Getenv("LINES"))
    156 	}
    157 	if h == 0 {
    158 		h = 25 // default
    159 	}
    160 	size.Width = w
    161 	size.Height = h
    162 	size.PixelWidth = int(ws.Xpixel)
    163 	size.PixelHeight = int(ws.Ypixel)
    164 	return size, nil
    165 }
    166 
    167 func (tty *devTty) NotifyResize(cb func()) {
    168 	tty.l.Lock()
    169 	tty.cb = cb
    170 	tty.l.Unlock()
    171 }
    172 
    173 // NewDevTty opens a /dev/tty based Tty.
    174 func NewDevTty() (Tty, error) {
    175 	return NewDevTtyFromDev("/dev/tty")
    176 }
    177 
    178 // NewDevTtyFromDev opens a tty device given a path.  This can be useful to bind to other nodes.
    179 func NewDevTtyFromDev(dev string) (Tty, error) {
    180 	tty := &devTty{
    181 		dev: dev,
    182 		sig: make(chan os.Signal),
    183 	}
    184 	var err error
    185 	if tty.of, err = os.OpenFile(dev, os.O_RDWR, 0); err != nil {
    186 		return nil, err
    187 	}
    188 	tty.fd = int(tty.of.Fd())
    189 	if !term.IsTerminal(tty.fd) {
    190 		_ = tty.f.Close()
    191 		return nil, errors.New("not a terminal")
    192 	}
    193 	if tty.saved, err = term.GetState(tty.fd); err != nil {
    194 		_ = tty.f.Close()
    195 		return nil, fmt.Errorf("failed to get state: %w", err)
    196 	}
    197 	return tty, nil
    198 }