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  }