github.com/wtfutil/wtf@v0.43.0/modules/cmdrunner/widget.go (about) 1 package cmdrunner 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "os" 8 "os/exec" 9 "strings" 10 "sync" 11 12 "github.com/creack/pty" 13 "github.com/rivo/tview" 14 "github.com/wtfutil/wtf/view" 15 ) 16 17 // Widget contains the data for this widget 18 type Widget struct { 19 view.TextWidget 20 21 settings *Settings 22 23 m sync.Mutex 24 buffer *bytes.Buffer 25 runChan chan bool 26 redrawChan chan bool 27 } 28 29 // NewWidget creates a new instance of the widget 30 func NewWidget(tviewApp *tview.Application, redrawChan chan bool, settings *Settings) *Widget { 31 widget := Widget{ 32 TextWidget: view.NewTextWidget(tviewApp, redrawChan, nil, settings.Common), 33 34 settings: settings, 35 buffer: &bytes.Buffer{}, 36 } 37 38 widget.View.SetWrap(true) 39 widget.View.SetScrollable(true) 40 41 widget.runChan = make(chan bool) 42 widget.redrawChan = make(chan bool) 43 go runCommandLoop(&widget) 44 go redrawLoop(&widget) 45 widget.runChan <- true 46 47 return &widget 48 } 49 50 // Refresh signals the runCommandLoop to continue, or triggers a re-draw if the 51 // command is still running. 52 func (widget *Widget) Refresh() { 53 // Try to run the command. If the command is still running, let it keep 54 // running and do a refresh instead. Otherwise, the widget will redraw when 55 // the command completes. 56 select { 57 case widget.runChan <- true: 58 default: 59 widget.redrawChan <- true 60 } 61 } 62 63 // String returns the string representation of the widget 64 func (widget *Widget) String() string { 65 args := strings.Join(widget.settings.args, " ") 66 67 if args != "" { 68 return fmt.Sprintf("%s %s", widget.settings.cmd, args) 69 } 70 71 return widget.settings.cmd 72 } 73 74 func (widget *Widget) Write(p []byte) (n int, err error) { 75 widget.m.Lock() 76 defer widget.m.Unlock() 77 78 // Write the new data into the buffer 79 n, err = widget.buffer.Write(p) 80 81 // Remove lines that exceed maxLines 82 lines := widget.countLines() 83 if lines > widget.settings.maxLines { 84 err = widget.drainLines(lines - widget.settings.maxLines) 85 } 86 87 return n, err 88 } 89 90 /* -------------------- Unexported Functions -------------------- */ 91 92 // countLines counts the lines of data in the buffer 93 func (widget *Widget) countLines() int { 94 return bytes.Count(widget.buffer.Bytes(), []byte{'\n'}) 95 } 96 97 // drainLines removed the first n lines from the buffer 98 func (widget *Widget) drainLines(n int) error { 99 for i := 0; i < n; i++ { 100 _, err := widget.buffer.ReadBytes('\n') 101 if err != nil { 102 return err 103 } 104 } 105 106 return nil 107 } 108 109 func (widget *Widget) environment() []string { 110 envs := os.Environ() 111 envs = append( 112 envs, 113 fmt.Sprintf("WTF_WIDGET_WIDTH=%d", widget.settings.width), 114 fmt.Sprintf("WTF_WIDGET_HEIGHT=%d", widget.settings.height), 115 ) 116 return envs 117 } 118 119 func runCommandLoop(widget *Widget) { 120 // Run the command forever in a loop. Refresh() will put a value into the 121 // channel to signal the loop to continue. 122 for { 123 <-widget.runChan 124 widget.resetBuffer() 125 cmd := exec.Command(widget.settings.cmd, widget.settings.args...) 126 cmd.Env = widget.environment() 127 cmd.Dir = widget.settings.workingDir 128 var err error 129 if widget.settings.pty { 130 err = runCommandPty(widget, cmd) 131 } else { 132 err = runCommand(widget, cmd) 133 } 134 if err != nil { 135 widget.handleError(err) 136 } 137 widget.redrawChan <- true 138 } 139 } 140 141 func runCommand(widget *Widget, cmd *exec.Cmd) error { 142 cmd.Stdout = widget 143 return cmd.Run() 144 } 145 146 func runCommandPty(widget *Widget, cmd *exec.Cmd) error { 147 f, err := pty.Start(cmd) 148 // The command has exited, print any error messages 149 if err != nil { 150 return err 151 } 152 153 _, err = io.Copy(widget.buffer, f) 154 if err != nil { 155 return err 156 } 157 return cmd.Wait() 158 } 159 160 func (widget *Widget) handleError(err error) { 161 widget.m.Lock() 162 defer widget.m.Unlock() 163 _, writeErr := widget.buffer.WriteString(err.Error()) 164 if writeErr != nil { 165 return 166 } 167 } 168 169 func redrawLoop(widget *Widget) { 170 for { 171 widget.Redraw(widget.content) 172 if widget.settings.tail { 173 widget.View.ScrollToEnd() 174 } 175 <-widget.redrawChan 176 } 177 } 178 179 func (widget *Widget) content() (string, string, bool) { 180 widget.m.Lock() 181 result := widget.buffer.String() 182 widget.m.Unlock() 183 184 ansiTitle := tview.TranslateANSI(tview.Escape(widget.CommonSettings().Title)) 185 if ansiTitle == defaultTitle { 186 ansiTitle = tview.TranslateANSI(tview.Escape(widget.String())) 187 } 188 ansiResult := tview.TranslateANSI(tview.Escape(result)) 189 190 return ansiTitle, ansiResult, false 191 } 192 193 func (widget *Widget) resetBuffer() { 194 widget.m.Lock() 195 defer widget.m.Unlock() 196 197 widget.buffer.Reset() 198 }