github.com/grahambrereton-form3/tilt@v0.10.18/internal/hud/renderer.go (about)

     1  package hud
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"strings"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/gdamore/tcell"
    11  
    12  	"github.com/windmilleng/tilt/internal/dockercompose"
    13  	"github.com/windmilleng/tilt/internal/hud/view"
    14  	"github.com/windmilleng/tilt/internal/rty"
    15  	"github.com/windmilleng/tilt/pkg/model"
    16  )
    17  
    18  type Renderer struct {
    19  	rty    rty.RTY
    20  	screen tcell.Screen
    21  	mu     *sync.Mutex
    22  	clock  func() time.Time
    23  }
    24  
    25  func NewRenderer(clock func() time.Time) *Renderer {
    26  	return &Renderer{
    27  		mu:    new(sync.Mutex),
    28  		clock: clock,
    29  	}
    30  }
    31  
    32  func (r *Renderer) Render(v view.View, vs view.ViewState) error {
    33  	r.mu.Lock()
    34  	defer r.mu.Unlock()
    35  	if r.rty != nil {
    36  		layout := r.layout(v, vs)
    37  		err := r.rty.Render(layout)
    38  		if err != nil {
    39  			return err
    40  		}
    41  	}
    42  	return nil
    43  }
    44  
    45  var cText = tcell.Color232
    46  var cLightText = tcell.Color243
    47  var cGood = tcell.ColorGreen
    48  var cBad = tcell.ColorRed
    49  var cPending = tcell.ColorYellow
    50  
    51  var statusColors = map[string]tcell.Color{
    52  	"Running":                          cGood,
    53  	"ContainerCreating":                cPending,
    54  	"Pending":                          cPending,
    55  	"PodInitializing":                  cPending,
    56  	"Error":                            cBad,
    57  	"CrashLoopBackOff":                 cBad,
    58  	"ErrImagePull":                     cBad,
    59  	"ImagePullBackOff":                 cBad,
    60  	"RunContainerError":                cBad,
    61  	"StartError":                       cBad,
    62  	string(dockercompose.StatusInProg): cPending,
    63  	string(dockercompose.StatusUp):     cGood,
    64  	string(dockercompose.StatusDown):   cBad,
    65  	"Completed":                        cGood,
    66  
    67  	// Placeholder. LocalResource has no runtime status; if build succeeds, should always be green
    68  	view.LocalResourceStatusPlaceholder: cGood,
    69  }
    70  
    71  func (r *Renderer) layout(v view.View, vs view.ViewState) rty.Component {
    72  	l := rty.NewFlexLayout(rty.DirVert)
    73  	if vs.ShowNarration {
    74  		l.Add(renderNarration(vs.NarrationMessage))
    75  		l.Add(rty.NewLine())
    76  	}
    77  
    78  	l.Add(r.renderResourceHeader(v))
    79  	l.Add(r.renderResources(v, vs))
    80  	l.Add(r.renderLogPane(v, vs))
    81  	l.Add(r.renderFooter(v, keyLegend(v, vs)))
    82  
    83  	var ret rty.Component = l
    84  
    85  	ret = r.maybeAddFullScreenLog(v, vs, ret)
    86  
    87  	ret = r.maybeAddAlertModal(v, vs, ret)
    88  
    89  	return ret
    90  }
    91  
    92  func (r *Renderer) maybeAddFullScreenLog(v view.View, vs view.ViewState, layout rty.Component) rty.Component {
    93  	if vs.TiltLogState == view.TiltLogFullScreen {
    94  		tabView := NewTabView(v, vs)
    95  
    96  		l := rty.NewConcatLayout(rty.DirVert)
    97  		sl := rty.NewTextScrollLayout("log")
    98  		l.Add(tabView.buildTabs(true))
    99  		sl.Add(rty.TextString(tabView.log()))
   100  		l.AddDynamic(sl)
   101  		l.Add(r.renderFooter(v, keyLegend(v, vs)))
   102  
   103  		layout = rty.NewModalLayout(layout, l, 1, true)
   104  	}
   105  	return layout
   106  }
   107  
   108  func (r *Renderer) maybeAddAlertModal(v view.View, vs view.ViewState, layout rty.Component) rty.Component {
   109  	alertMsg := ""
   110  	if v.FatalError != nil {
   111  		alertMsg = fmt.Sprintf("Tilt has encountered a fatal error: %s\nOnce you fix this issue you'll need to restart Tilt. In the meantime feel free to browse through the UI.", v.FatalError.Error())
   112  	} else if vs.AlertMessage != "" {
   113  		alertMsg = vs.AlertMessage
   114  	}
   115  
   116  	if alertMsg != "" {
   117  		l := rty.NewLines()
   118  		l.Add(rty.TextString(""))
   119  
   120  		msg := "   " + alertMsg + "   "
   121  		l.Add(rty.Fg(rty.TextString(msg), tcell.ColorDefault))
   122  		l.Add(rty.TextString(""))
   123  
   124  		w := rty.NewWindow(l)
   125  		w.SetTitle("! Alert !")
   126  		layout = r.renderModal(rty.Fg(w, tcell.ColorRed), layout, false)
   127  	}
   128  	return layout
   129  }
   130  
   131  func (r *Renderer) renderLogPane(v view.View, vs view.ViewState) rty.Component {
   132  	tabView := NewTabView(v, vs)
   133  	var height int
   134  	switch vs.TiltLogState {
   135  	case view.TiltLogShort:
   136  		height = 8
   137  	case view.TiltLogHalfScreen:
   138  		height = rty.GROW
   139  	case view.TiltLogFullScreen:
   140  		height = 1
   141  		// FullScreen is handled elsewhere, since it's no longer a pane
   142  		// but we have to set height to something non-0 or rty will blow up
   143  	}
   144  	return rty.NewFixedSize(tabView.Build(), rty.GROW, height)
   145  }
   146  
   147  func renderPaneHeader(isMax bool) rty.Component {
   148  	var verb string
   149  	if isMax {
   150  		verb = "contract"
   151  	} else {
   152  		verb = "expand"
   153  	}
   154  	s := fmt.Sprintf("X: %s", verb)
   155  	l := rty.NewLine()
   156  	l.Add(rty.NewFillerString(' '))
   157  	l.Add(rty.TextString(fmt.Sprintf(" %s ", s)))
   158  	return l
   159  }
   160  
   161  func (r *Renderer) renderStatusBar(v view.View) rty.Component {
   162  	sb := rty.NewStringBuilder()
   163  	sb.Text(" ") // Indent
   164  	errorCount := 0
   165  	for _, res := range v.Resources {
   166  		if isInError(res) {
   167  			errorCount++
   168  		}
   169  	}
   170  	if errorCount == 0 && v.TiltfileErrorMessage() == "" {
   171  		sb.Fg(cGood).Text("✓").Fg(tcell.ColorDefault).Fg(cText).Text(" OK").Fg(tcell.ColorDefault)
   172  	} else {
   173  		var errorCountMessage string
   174  		var tiltfileError strings.Builder
   175  		s := "error"
   176  		if errorCount > 1 {
   177  			s = "errors"
   178  		}
   179  
   180  		if errorCount > 0 {
   181  			errorCountMessage = fmt.Sprintf(" %d %s", errorCount, s)
   182  		}
   183  
   184  		if v.TiltfileErrorMessage() != "" {
   185  			_, _ = tiltfileError.WriteString(" • Tiltfile error")
   186  		}
   187  		sb.Fg(cBad).Text("✖").Fg(tcell.ColorDefault).Fg(cText).Textf("%s%s", errorCountMessage, tiltfileError.String()).Fg(tcell.ColorDefault)
   188  	}
   189  	return rty.Bg(rty.OneLine(sb.Build()), tcell.ColorWhiteSmoke)
   190  }
   191  
   192  func (r *Renderer) renderFooter(v view.View, keys string) rty.Component {
   193  	footer := rty.NewConcatLayout(rty.DirVert)
   194  	footer.Add(r.renderStatusBar(v))
   195  	l := rty.NewConcatLayout(rty.DirHor)
   196  	sbRight := rty.NewStringBuilder()
   197  	sbRight.Text(keys)
   198  	l.AddDynamic(rty.NewFillerString(' '))
   199  	l.Add(sbRight.Build())
   200  	footer.Add(l)
   201  	return rty.NewFixedSize(footer, rty.GROW, 2)
   202  }
   203  
   204  func keyLegend(v view.View, vs view.ViewState) string {
   205  	defaultKeys := "Browse (↓ ↑), Expand (→) ┊ (enter) log, (b)rowser ┊ (ctrl-C) quit  "
   206  	if vs.AlertMessage != "" {
   207  		return "Tilt (l)og ┊ (esc) close alert "
   208  	}
   209  	return defaultKeys
   210  }
   211  
   212  func isInError(res view.Resource) bool {
   213  	return statusColor(res) == cBad
   214  }
   215  
   216  func warnings(res view.Resource) []string {
   217  	return res.LastBuild().Warnings
   218  }
   219  
   220  func isCrashing(res view.Resource) bool {
   221  	return (res.IsK8s() && res.K8sInfo().PodRestarts > 0) ||
   222  		res.LastBuild().Reason.Has(model.BuildReasonFlagCrash) ||
   223  		res.CurrentBuild.Reason.Has(model.BuildReasonFlagCrash) ||
   224  		res.PendingBuildReason.Has(model.BuildReasonFlagCrash) ||
   225  		res.IsDC() && res.DockerComposeTarget().Status() == string(dockercompose.StatusCrash)
   226  }
   227  
   228  func (r *Renderer) renderModal(fg rty.Component, bg rty.Component, fixed bool) rty.Component {
   229  	return rty.NewModalLayout(bg, fg, .9, fixed)
   230  }
   231  
   232  func renderNarration(msg string) rty.Component {
   233  	lines := rty.NewLines()
   234  	l := rty.NewLine()
   235  	l.Add(rty.TextString(msg))
   236  	lines.Add(rty.NewLine())
   237  	lines.Add(l)
   238  	lines.Add(rty.NewLine())
   239  
   240  	box := rty.Fg(rty.Bg(lines, tcell.ColorLightGrey), cText)
   241  	return rty.NewFixedSize(box, rty.GROW, 3)
   242  }
   243  
   244  func (r *Renderer) renderResourceHeader(v view.View) rty.Component {
   245  	l := rty.NewConcatLayout(rty.DirHor)
   246  	l.Add(rty.ColoredString("  RESOURCE NAME ", cLightText))
   247  	l.AddDynamic(rty.NewFillerString(' '))
   248  
   249  	k8sCell := rty.ColoredString(" CONTAINER", cLightText)
   250  	l.Add(k8sCell)
   251  	l.Add(middotText())
   252  
   253  	buildCell := rty.NewMinLengthLayout(BuildDurCellMinWidth+BuildStatusCellMinWidth, rty.DirHor).
   254  		SetAlign(rty.AlignEnd).
   255  		Add(rty.ColoredString("UPDATE STATUS ", cLightText))
   256  	l.Add(buildCell)
   257  	l.Add(middotText())
   258  	deployCell := rty.NewMinLengthLayout(DeployCellMinWidth+1, rty.DirHor).
   259  		SetAlign(rty.AlignEnd).
   260  		Add(rty.ColoredString("AS OF ", cLightText))
   261  	l.Add(deployCell)
   262  	return rty.OneLine(l)
   263  }
   264  
   265  func (r *Renderer) renderResources(v view.View, vs view.ViewState) rty.Component {
   266  	rs := v.Resources
   267  
   268  	cl := rty.NewConcatLayout(rty.DirVert)
   269  
   270  	childNames := make([]string, len(rs))
   271  	for i, r := range rs {
   272  		childNames[i] = r.Name.String()
   273  	}
   274  	// the items added to `l` below must be kept in sync with `childNames` above
   275  	l, selectedResource := r.rty.RegisterElementScroll(resourcesScollerName, childNames)
   276  
   277  	if len(rs) > 0 {
   278  		for i, res := range rs {
   279  			l.Add(r.renderResource(res, vs.Resources[i], res.TriggerMode, selectedResource == res.Name.String()))
   280  		}
   281  	}
   282  
   283  	cl.Add(l)
   284  	return cl
   285  }
   286  
   287  func (r *Renderer) renderResource(res view.Resource, rv view.ResourceViewState, triggerMode model.TriggerMode, selected bool) rty.Component {
   288  	return NewResourceView(res, rv, triggerMode, selected, r.clock).Build()
   289  }
   290  
   291  func (r *Renderer) SetUp() (chan tcell.Event, error) {
   292  	r.mu.Lock()
   293  	defer r.mu.Unlock()
   294  
   295  	screen, err := tcell.NewScreen()
   296  	if err != nil {
   297  		if err == tcell.ErrTermNotFound {
   298  			// The statically-compiled tcell only supports the most common TERM configs.
   299  			// The dynamically-compiled tcell supports more, but has distribution problems.
   300  			// See: https://github.com/gdamore/tcell/issues/252
   301  			term := os.Getenv("TERM")
   302  			return nil, fmt.Errorf("Tilt does not support TERM=%q. "+
   303  				"This is not a common Terminal config. "+
   304  				"If you expect that you're using a common terminal, "+
   305  				"you might have misconfigured $TERM in your .profile.", term)
   306  		}
   307  		return nil, err
   308  	}
   309  	if err = screen.Init(); err != nil {
   310  		return nil, err
   311  	}
   312  	screenEvents := make(chan tcell.Event)
   313  	go func() {
   314  		for {
   315  			screenEvents <- screen.PollEvent()
   316  		}
   317  	}()
   318  
   319  	r.rty = rty.NewRTY(screen)
   320  
   321  	r.screen = screen
   322  
   323  	return screenEvents, nil
   324  }
   325  
   326  func (r *Renderer) Reset() {
   327  	r.mu.Lock()
   328  	defer r.mu.Unlock()
   329  
   330  	if r.screen != nil {
   331  		r.screen.Fini()
   332  	}
   333  
   334  	r.screen = nil
   335  }