github.com/grahambrereton-form3/tilt@v0.10.18/internal/hud/hud.go (about) 1 package hud 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "runtime" 8 "runtime/pprof" 9 "strconv" 10 "sync" 11 "time" 12 13 "github.com/windmilleng/tilt/pkg/logger" 14 15 "github.com/gdamore/tcell" 16 "github.com/pkg/browser" 17 "github.com/pkg/errors" 18 19 "github.com/windmilleng/tilt/internal/analytics" 20 "github.com/windmilleng/tilt/internal/hud/view" 21 "github.com/windmilleng/tilt/internal/store" 22 "github.com/windmilleng/tilt/pkg/model" 23 ) 24 25 // The main loop ensures the HUD updates at least this often 26 const DefaultRefreshInterval = 100 * time.Millisecond 27 28 // number of arrows a pgup/dn is equivalent to 29 // (we don't currently worry about trying to know how big a page is, and instead just support pgup/dn as "faster arrows" 30 const pgUpDownCount = 20 31 32 type HeadsUpDisplay interface { 33 store.Subscriber 34 35 Run(ctx context.Context, dispatch func(action store.Action), refreshRate time.Duration) error 36 Update(v view.View, vs view.ViewState) error 37 Close() 38 SetNarrationMessage(ctx context.Context, msg string) error 39 } 40 41 type Hud struct { 42 r *Renderer 43 webURL model.WebURL 44 45 currentView view.View 46 currentViewState view.ViewState 47 mu sync.RWMutex 48 isRunning bool 49 a *analytics.TiltAnalytics 50 } 51 52 var _ HeadsUpDisplay = (*Hud)(nil) 53 54 func NewDefaultHeadsUpDisplay(renderer *Renderer, webURL model.WebURL, analytics *analytics.TiltAnalytics) (HeadsUpDisplay, error) { 55 return &Hud{ 56 r: renderer, 57 webURL: webURL, 58 a: analytics, 59 }, nil 60 } 61 62 func (h *Hud) SetNarrationMessage(ctx context.Context, msg string) error { 63 h.mu.Lock() 64 defer h.mu.Unlock() 65 currentViewState := h.currentViewState 66 currentViewState.ShowNarration = true 67 currentViewState.NarrationMessage = msg 68 return h.setViewState(ctx, currentViewState) 69 } 70 71 func (h *Hud) Run(ctx context.Context, dispatch func(action store.Action), refreshRate time.Duration) error { 72 h.mu.Lock() 73 h.isRunning = true 74 h.mu.Unlock() 75 76 defer func() { 77 h.mu.Lock() 78 h.isRunning = false 79 h.mu.Unlock() 80 }() 81 82 screenEvents, err := h.r.SetUp() 83 if err != nil { 84 return errors.Wrap(err, "setting up screen") 85 } 86 87 defer h.Close() 88 89 if refreshRate == 0 { 90 refreshRate = DefaultRefreshInterval 91 } 92 ticker := time.NewTicker(refreshRate) 93 94 for { 95 select { 96 case <-ctx.Done(): 97 err := ctx.Err() 98 if err != context.Canceled { 99 return err 100 } else { 101 return nil 102 } 103 case e := <-screenEvents: 104 done := h.handleScreenEvent(ctx, dispatch, e) 105 if done { 106 return nil 107 } 108 case <-ticker.C: 109 err := h.Refresh(ctx) 110 if err != nil { 111 return err 112 } 113 } 114 } 115 } 116 117 func (h *Hud) Close() { 118 h.r.Reset() 119 } 120 121 func (h *Hud) recordInteraction(name string) { 122 h.a.Incr(fmt.Sprintf("ui.interactions.%s", name), map[string]string{}) 123 } 124 125 func (h *Hud) handleScreenEvent(ctx context.Context, dispatch func(action store.Action), ev tcell.Event) (done bool) { 126 h.mu.Lock() 127 defer h.mu.Unlock() 128 129 escape := func() { 130 am := h.activeModal() 131 if am != nil { 132 am.Close(&h.currentViewState) 133 } 134 } 135 136 switch ev := ev.(type) { 137 case *tcell.EventKey: 138 switch ev.Key() { 139 case tcell.KeyEscape: 140 escape() 141 case tcell.KeyRune: 142 switch r := ev.Rune(); { 143 case r == 'b': // [B]rowser 144 // If we have an endpoint(s), open the first one 145 // TODO(nick): We might need some hints on what load balancer to 146 // open if we have multiple, or what path to default to on the opened manifest. 147 _, selected := h.selectedResource() 148 if len(selected.Endpoints) > 0 { 149 h.recordInteraction("open_preview") 150 err := browser.OpenURL(selected.Endpoints[0]) 151 if err != nil { 152 h.currentViewState.AlertMessage = fmt.Sprintf("error opening url '%s' for resource '%s': %v", 153 selected.Endpoints[0], selected.Name, err) 154 } 155 } else { 156 h.currentViewState.AlertMessage = fmt.Sprintf("no urls for resource '%s' ¯\\_(ツ)_/¯", selected.Name) 157 } 158 case r == 'l': // Tilt [L]og 159 if h.webURL.Empty() { 160 break 161 } 162 url := h.webURL 163 url.Path = "/" 164 _ = browser.OpenURL(url.String()) 165 case r == 'k': 166 h.activeScroller().Up() 167 h.refreshSelectedIndex() 168 case r == 'j': 169 h.activeScroller().Down() 170 h.refreshSelectedIndex() 171 case r == 'q': // [Q]uit 172 escape() 173 case r == 'R': // hidden key for recovering from printf junk during demos 174 h.r.screen.Sync() 175 case r == 'x': 176 h.recordInteraction("cycle_view_log_state") 177 h.currentViewState.CycleViewLogState() 178 case r == '1': 179 h.recordInteraction("tab_all_log") 180 h.currentViewState.TabState = view.TabAllLog 181 case r == '2': 182 h.recordInteraction("tab_build_log") 183 h.currentViewState.TabState = view.TabBuildLog 184 case r == '3': 185 h.recordInteraction("tab_pod_log") 186 h.currentViewState.TabState = view.TabPodLog 187 } 188 case tcell.KeyUp: 189 h.activeScroller().Up() 190 h.refreshSelectedIndex() 191 case tcell.KeyDown: 192 h.activeScroller().Down() 193 h.refreshSelectedIndex() 194 case tcell.KeyPgUp: 195 for i := 0; i < pgUpDownCount; i++ { 196 h.activeScroller().Up() 197 } 198 h.refreshSelectedIndex() 199 case tcell.KeyPgDn: 200 for i := 0; i < pgUpDownCount; i++ { 201 h.activeScroller().Down() 202 } 203 h.refreshSelectedIndex() 204 case tcell.KeyEnter: 205 if len(h.currentView.Resources) == 0 { 206 break 207 } 208 _, r := h.selectedResource() 209 210 if h.webURL.Empty() { 211 break 212 } 213 url := h.webURL 214 url.Path = fmt.Sprintf("/r/%s/", r.Name) 215 h.a.Incr("ui.interactions.open_log", map[string]string{"is_tiltfile": strconv.FormatBool(r.Name == store.TiltfileManifestName)}) 216 _ = browser.OpenURL(url.String()) 217 case tcell.KeyRight: 218 i, _ := h.selectedResource() 219 h.currentViewState.Resources[i].CollapseState = view.CollapseNo 220 case tcell.KeyLeft: 221 i, _ := h.selectedResource() 222 h.currentViewState.Resources[i].CollapseState = view.CollapseYes 223 case tcell.KeyHome: 224 h.activeScroller().Top() 225 case tcell.KeyEnd: 226 h.activeScroller().Bottom() 227 case tcell.KeyCtrlC: 228 h.Close() 229 dispatch(NewExitAction(nil)) 230 return true 231 case tcell.KeyCtrlD: 232 dispatch(DumpEngineStateAction{}) 233 case tcell.KeyCtrlP: 234 if h.currentView.IsProfiling { 235 dispatch(StopProfilingAction{}) 236 } else { 237 dispatch(StartProfilingAction{}) 238 } 239 case tcell.KeyCtrlO: 240 go writeHeapProfile(ctx) 241 case tcell.KeyCtrlT: 242 dispatch(SetLogTimestampsAction{!h.currentView.LogTimestamps}) 243 } 244 245 case *tcell.EventResize: 246 // since we already refresh after the switch, don't need to do anything here 247 // just marking this as where sigwinch gets handled 248 } 249 250 err := h.refresh(ctx) 251 if err != nil { 252 dispatch(NewExitAction(err)) 253 } 254 255 return false 256 } 257 258 func (h *Hud) OnChange(ctx context.Context, st store.RStore) { 259 state := st.RLockState() 260 view := store.StateToView(state) 261 st.RUnlockState() 262 263 h.mu.Lock() 264 defer h.mu.Unlock() 265 err := h.setView(ctx, view) 266 if err != nil { 267 st.Dispatch(NewExitAction(err)) 268 } 269 } 270 271 func (h *Hud) Refresh(ctx context.Context) error { 272 h.mu.Lock() 273 defer h.mu.Unlock() 274 return h.refresh(ctx) 275 } 276 277 // Must hold the lock 278 func (h *Hud) setView(ctx context.Context, view view.View) error { 279 // if we're going from 1 resource (i.e., the Tiltfile) to more than 1, reset 280 // the resource selection, so that we're not scrolled to the bottom with the Tiltfile selected 281 if len(h.currentView.Resources) == 1 && len(view.Resources) > 1 { 282 h.resetResourceSelection() 283 } 284 h.currentView = view 285 h.refreshSelectedIndex() 286 287 // if the hud isn't running, make sure new logs are visible on stdout 288 logLen := view.Log.Len() 289 if !h.isRunning && h.currentViewState.ProcessedLogByteCount < logLen { 290 fmt.Print(view.Log.String()[h.currentViewState.ProcessedLogByteCount:]) 291 } 292 293 h.currentViewState.ProcessedLogByteCount = logLen 294 295 return h.refresh(ctx) 296 } 297 298 // Must hold the lock 299 func (h *Hud) setViewState(ctx context.Context, currentViewState view.ViewState) error { 300 h.currentViewState = currentViewState 301 return h.refresh(ctx) 302 } 303 304 // Must hold the lock 305 func (h *Hud) refresh(ctx context.Context) error { 306 // TODO: We don't handle the order of resources changing 307 for len(h.currentViewState.Resources) < len(h.currentView.Resources) { 308 h.currentViewState.Resources = append(h.currentViewState.Resources, view.ResourceViewState{}) 309 } 310 311 vs := h.currentViewState 312 vs.Resources = append(vs.Resources, h.currentViewState.Resources...) 313 314 return h.Update(h.currentView, h.currentViewState) 315 } 316 317 func (h *Hud) Update(v view.View, vs view.ViewState) error { 318 err := h.r.Render(v, vs) 319 return errors.Wrap(err, "error rendering hud") 320 } 321 322 func (h *Hud) resetResourceSelection() { 323 rty := h.r.rty 324 if rty == nil { 325 return 326 } 327 // wipe out any scroll/selection state for resources 328 // it will get re-set in the next call to render 329 rty.RegisterElementScroll("resources", []string{}) 330 } 331 332 func (h *Hud) refreshSelectedIndex() { 333 rty := h.r.rty 334 if rty == nil { 335 return 336 } 337 scroller := rty.ElementScroller("resources") 338 if scroller == nil { 339 return 340 } 341 i := scroller.GetSelectedIndex() 342 h.currentViewState.SelectedIndex = i 343 } 344 345 func (h *Hud) selectedResource() (i int, resource view.Resource) { 346 return selectedResource(h.currentView, h.currentViewState) 347 } 348 349 func selectedResource(view view.View, state view.ViewState) (i int, resource view.Resource) { 350 i = state.SelectedIndex 351 if i >= 0 && i < len(view.Resources) { 352 resource = view.Resources[i] 353 } 354 return i, resource 355 356 } 357 358 var _ store.Subscriber = &Hud{} 359 360 func writeHeapProfile(ctx context.Context) { 361 f, err := os.Create("tilt.heap_profile") 362 if err != nil { 363 logger.Get(ctx).Infof("error creating file for heap profile: %v", err) 364 return 365 } 366 runtime.GC() 367 logger.Get(ctx).Infof("writing heap profile to %s", f.Name()) 368 err = pprof.WriteHeapProfile(f) 369 if err != nil { 370 logger.Get(ctx).Infof("error writing heap profile: %v", err) 371 return 372 } 373 err = f.Close() 374 if err != nil { 375 logger.Get(ctx).Infof("error closing file for heap profile: %v", err) 376 return 377 } 378 logger.Get(ctx).Infof("wrote heap profile to %s", f.Name()) 379 }