gemini-browser

A text-based gemini browser
git clone git://git.laack.co/gemini-browser.git
Log | Files | Refs | README

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 }