...

Source file src/github.com/gdamore/tcell/v2/stdin_unix.go

Documentation: github.com/gdamore/tcell/v2

     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/term"
    31  )
    32  
    33  // stdIoTty is an implementation of the Tty API based upon stdin/stdout.
    34  type stdIoTty struct {
    35  	fd    int
    36  	in    *os.File
    37  	out   *os.File
    38  	saved *term.State
    39  	sig   chan os.Signal
    40  	cb    func()
    41  	stopQ chan struct{}
    42  	dev   string
    43  	wg    sync.WaitGroup
    44  	l     sync.Mutex
    45  }
    46  
    47  func (tty *stdIoTty) Read(b []byte) (int, error) {
    48  	return tty.in.Read(b)
    49  }
    50  
    51  func (tty *stdIoTty) Write(b []byte) (int, error) {
    52  	return tty.out.Write(b)
    53  }
    54  
    55  func (tty *stdIoTty) Close() error {
    56  	return nil
    57  }
    58  
    59  func (tty *stdIoTty) Start() error {
    60  	tty.l.Lock()
    61  	defer tty.l.Unlock()
    62  
    63  	// We open another copy of /dev/tty.  This is a workaround for unusual behavior
    64  	// observed in macOS, apparently caused when a sub-shell (for example) closes our
    65  	// own tty device (when it exits for example).  Getting a fresh new one seems to
    66  	// resolve the problem.  (We believe this is a bug in the macOS tty driver that
    67  	// fails to account for dup() references to the same file before applying close()
    68  	// related behaviors to the tty.)  We're also holding the original copy we opened
    69  	// since closing that might have deleterious effects as well.  The upshot is that
    70  	// we will have up to two separate file handles open on /dev/tty.  (Note that when
    71  	// using stdin/stdout instead of /dev/tty this problem is not observed.)
    72  	var err error
    73  	tty.in = os.Stdin
    74  	tty.out = os.Stdout
    75  	tty.fd = int(tty.in.Fd())
    76  
    77  	if !term.IsTerminal(tty.fd) {
    78  		return errors.New("device is not a terminal")
    79  	}
    80  
    81  	_ = tty.in.SetReadDeadline(time.Time{})
    82  	saved, err := term.MakeRaw(tty.fd) // also sets vMin and vTime
    83  	if err != nil {
    84  		return err
    85  	}
    86  	tty.saved = saved
    87  
    88  	tty.stopQ = make(chan struct{})
    89  	tty.wg.Add(1)
    90  	go func(stopQ chan struct{}) {
    91  		defer tty.wg.Done()
    92  		for {
    93  			select {
    94  			case <-tty.sig:
    95  				tty.l.Lock()
    96  				cb := tty.cb
    97  				tty.l.Unlock()
    98  				if cb != nil {
    99  					cb()
   100  				}
   101  			case <-stopQ:
   102  				return
   103  			}
   104  		}
   105  	}(tty.stopQ)
   106  
   107  	signal.Notify(tty.sig, syscall.SIGWINCH)
   108  	return nil
   109  }
   110  
   111  func (tty *stdIoTty) Drain() error {
   112  	_ = tty.in.SetReadDeadline(time.Now())
   113  	if err := tcSetBufParams(tty.fd, 0, 0); err != nil {
   114  		return err
   115  	}
   116  	return nil
   117  }
   118  
   119  func (tty *stdIoTty) Stop() error {
   120  	tty.l.Lock()
   121  	if err := term.Restore(tty.fd, tty.saved); err != nil {
   122  		tty.l.Unlock()
   123  		return err
   124  	}
   125  	_ = tty.in.SetReadDeadline(time.Now())
   126  
   127  	signal.Stop(tty.sig)
   128  	close(tty.stopQ)
   129  	tty.l.Unlock()
   130  
   131  	tty.wg.Wait()
   132  
   133  	return nil
   134  }
   135  
   136  func (tty *stdIoTty) WindowSize() (int, int, error) {
   137  	w, h, err := term.GetSize(tty.fd)
   138  	if err != nil {
   139  		return 0, 0, err
   140  	}
   141  	if w == 0 {
   142  		w, _ = strconv.Atoi(os.Getenv("COLUMNS"))
   143  	}
   144  	if w == 0 {
   145  		w = 80 // default
   146  	}
   147  	if h == 0 {
   148  		h, _ = strconv.Atoi(os.Getenv("LINES"))
   149  	}
   150  	if h == 0 {
   151  		h = 25 // default
   152  	}
   153  	return w, h, nil
   154  }
   155  
   156  func (tty *stdIoTty) NotifyResize(cb func()) {
   157  	tty.l.Lock()
   158  	tty.cb = cb
   159  	tty.l.Unlock()
   160  }
   161  
   162  // NewStdioTty opens a tty using standard input/output.
   163  func NewStdIoTty() (Tty, error) {
   164  	tty := &stdIoTty{
   165  		sig: make(chan os.Signal),
   166  		in:  os.Stdin,
   167  		out: os.Stdout,
   168  	}
   169  	var err error
   170  	tty.fd = int(tty.in.Fd())
   171  	if !term.IsTerminal(tty.fd) {
   172  		return nil, errors.New("not a terminal")
   173  	}
   174  	if tty.saved, err = term.GetState(tty.fd); err != nil {
   175  		return nil, fmt.Errorf("failed to get state: %w", err)
   176  	}
   177  	return tty, nil
   178  }
   179  

View as plain text