gotest.tools/gotestsum@v1.11.0/internal/filewatcher/term_unix.go (about)

     1  //go:build !windows && !aix
     2  // +build !windows,!aix
     3  
     4  package filewatcher
     5  
     6  import (
     7  	"bufio"
     8  	"context"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  
    13  	"golang.org/x/sys/unix"
    14  	"gotest.tools/gotestsum/internal/log"
    15  )
    16  
    17  type terminal struct {
    18  	ch    chan Event
    19  	reset func()
    20  }
    21  
    22  func newTerminal() *terminal {
    23  	h := &terminal{ch: make(chan Event)}
    24  	h.Start()
    25  	return h
    26  }
    27  
    28  // Start the terminal is non-blocking read mode. The terminal can be reset to
    29  // normal mode by calling Reset.
    30  func (r *terminal) Start() {
    31  	if r == nil {
    32  		return
    33  	}
    34  	fd := int(os.Stdin.Fd())
    35  	reset, err := enableNonBlockingRead(fd)
    36  	if err != nil {
    37  		log.Warnf("failed to put terminal (fd %d) into raw mode: %v", fd, err)
    38  		return
    39  	}
    40  	r.reset = reset
    41  }
    42  
    43  func enableNonBlockingRead(fd int) (func(), error) {
    44  	term, err := unix.IoctlGetTermios(fd, tcGet)
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  
    49  	state := *term
    50  	reset := func() {
    51  		if err := unix.IoctlSetTermios(fd, tcSet, &state); err != nil {
    52  			log.Debugf("failed to reset fd %d: %v", fd, err)
    53  		}
    54  	}
    55  
    56  	term.Lflag &^= unix.ECHO | unix.ICANON
    57  	term.Cc[unix.VMIN] = 1
    58  	term.Cc[unix.VTIME] = 0
    59  	if err := unix.IoctlSetTermios(fd, tcSet, term); err != nil {
    60  		reset()
    61  		return nil, err
    62  	}
    63  	return reset, nil
    64  }
    65  
    66  var stdin io.Reader = os.Stdin
    67  
    68  // Monitor the terminal for key presses. If the key press is associated with an
    69  // action, an event will be sent to channel returned by Events.
    70  func (r *terminal) Monitor(ctx context.Context) {
    71  	if r == nil {
    72  		return
    73  	}
    74  	in := bufio.NewReader(stdin)
    75  	for {
    76  		char, err := in.ReadByte()
    77  		if err != nil {
    78  			log.Warnf("failed to read input: %v", err)
    79  			return
    80  		}
    81  		log.Debugf("received byte %v (%v)", char, string(char))
    82  
    83  		chResume := make(chan struct{})
    84  		switch char {
    85  		case 'r':
    86  			r.ch <- Event{resume: chResume, useLastPath: true}
    87  		case 'd':
    88  			r.ch <- Event{resume: chResume, useLastPath: true, Debug: true}
    89  		case 'a':
    90  			r.ch <- Event{resume: chResume, PkgPath: "./..."}
    91  		case 'l':
    92  			r.ch <- Event{resume: chResume, reloadPaths: true}
    93  		case 'u':
    94  			r.ch <- Event{resume: chResume, useLastPath: true, Args: []string{"-update"}}
    95  		case '\n':
    96  			fmt.Println()
    97  			continue
    98  		default:
    99  			continue
   100  		}
   101  
   102  		select {
   103  		case <-ctx.Done():
   104  			return
   105  		case <-chResume:
   106  		}
   107  	}
   108  }
   109  
   110  // Events returns a channel which will receive events when keys are pressed.
   111  // When an event is received, the caller must close the resume channel to
   112  // resume monitoring for events.
   113  func (r *terminal) Events() <-chan Event {
   114  	if r == nil {
   115  		return nil
   116  	}
   117  	return r.ch
   118  }
   119  
   120  func (r *terminal) Reset() {
   121  	if r != nil && r.reset != nil {
   122  		r.reset()
   123  	}
   124  }