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 }