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 }