github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/backend/display/tree.go (about) 1 // Copyright 2016-2022, Pulumi Corporation. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // nolint: goconst 16 package display 17 18 import ( 19 "errors" 20 "fmt" 21 "io" 22 "strings" 23 "sync" 24 "time" 25 "unicode/utf8" 26 27 "github.com/pulumi/pulumi/pkg/v3/backend/display/internal/terminal" 28 "github.com/pulumi/pulumi/pkg/v3/engine" 29 "github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors" 30 "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" 31 "github.com/rivo/uniseg" 32 ) 33 34 type treeRenderer struct { 35 m sync.Mutex 36 37 opts Options 38 39 term terminal.Terminal 40 41 dirty bool // True if the display has changed since the last redraw. 42 rewind int // The number of lines we need to rewind to redraw the entire screen. 43 44 treeTableRows []string 45 systemMessages []string 46 47 ticker *time.Ticker 48 keys chan string 49 closed chan bool 50 51 treeTableOffset int // The scroll offset into the tree table. 52 maxTreeTableOffset int // The maximum scroll offset. 53 } 54 55 func newInteractiveRenderer(term terminal.Terminal, opts Options) progressRenderer { 56 // Something about the tree renderer--possibly the raw terminal--does not yet play well with Windows, so for now 57 // we fall back to the legacy renderer on that platform. 58 if !term.IsRaw() { 59 return newInteractiveMessageRenderer(term, opts) 60 } 61 62 r := &treeRenderer{ 63 opts: opts, 64 term: term, 65 ticker: time.NewTicker(16 * time.Millisecond), 66 keys: make(chan string), 67 closed: make(chan bool), 68 } 69 if opts.deterministicOutput { 70 r.ticker.Stop() 71 } 72 go r.handleEvents() 73 go r.pollInput() 74 return r 75 } 76 77 func (r *treeRenderer) Close() error { 78 return r.term.Close() 79 } 80 81 func (r *treeRenderer) tick(display *ProgressDisplay) { 82 r.render(display) 83 } 84 85 func (r *treeRenderer) rowUpdated(display *ProgressDisplay, _ Row) { 86 r.render(display) 87 } 88 89 func (r *treeRenderer) systemMessage(display *ProgressDisplay, _ engine.StdoutEventPayload) { 90 r.render(display) 91 } 92 93 func (r *treeRenderer) done(display *ProgressDisplay) { 94 r.render(display) 95 96 r.ticker.Stop() 97 r.closed <- true 98 close(r.closed) 99 100 r.frame(false, true) 101 } 102 103 func (r *treeRenderer) print(text string) { 104 _, err := r.term.Write([]byte(r.opts.Color.Colorize(text))) 105 contract.IgnoreError(err) 106 } 107 108 func (r *treeRenderer) println(display *ProgressDisplay, text string) { 109 r.print(text) 110 r.print("\n") 111 } 112 113 func (r *treeRenderer) render(display *ProgressDisplay) { 114 r.m.Lock() 115 defer r.m.Unlock() 116 117 if display.headerRow == nil { 118 return 119 } 120 121 // Render the resource tree table into rows. 122 rootNodes := display.generateTreeNodes() 123 rootNodes = display.filterOutUnnecessaryNodesAndSetDisplayTimes(rootNodes) 124 sortNodes(rootNodes) 125 display.addIndentations(rootNodes, true /*isRoot*/, "") 126 127 maxSuffixLength := 0 128 for _, v := range display.suffixesArray { 129 runeCount := utf8.RuneCountInString(v) 130 if runeCount > maxSuffixLength { 131 maxSuffixLength = runeCount 132 } 133 } 134 135 var treeTableRows [][]string 136 var maxColumnLengths []int 137 display.convertNodesToRows(rootNodes, maxSuffixLength, &treeTableRows, &maxColumnLengths) 138 removeInfoColumnIfUnneeded(treeTableRows) 139 140 r.treeTableRows = r.treeTableRows[:0] 141 for _, row := range treeTableRows { 142 rendered := renderRow(row, maxColumnLengths) 143 r.treeTableRows = append(r.treeTableRows, rendered) 144 } 145 146 // Convert system events into lines. 147 r.systemMessages = r.systemMessages[:0] 148 for _, payload := range display.systemEventPayloads { 149 msg := payload.Color.Colorize(payload.Message) 150 r.systemMessages = append(r.systemMessages, splitIntoDisplayableLines(msg)...) 151 } 152 153 r.dirty = true 154 if r.opts.deterministicOutput { 155 r.frame(true, false) 156 } 157 } 158 159 func (r *treeRenderer) markDirty() { 160 r.m.Lock() 161 defer r.m.Unlock() 162 163 r.dirty = true 164 } 165 166 // +--------------------------------------------+ 167 // | treetable header | 168 // | treetable contents... | 169 // | treetable footer | 170 // | system messages header | 171 // | system messages contents... | 172 // +--------------------------------------------+ 173 func (r *treeRenderer) frame(locked, done bool) { 174 if !locked { 175 r.m.Lock() 176 defer r.m.Unlock() 177 } 178 179 if !done && !r.dirty { 180 return 181 } 182 r.dirty = false 183 184 termWidth, termHeight, err := r.term.Size() 185 contract.IgnoreError(err) 186 187 treeTableRows := r.treeTableRows 188 systemMessages := r.systemMessages 189 190 var treeTableHeight int 191 var treeTableHeader string 192 if len(r.treeTableRows) > 0 { 193 treeTableHeader, treeTableRows = treeTableRows[0], treeTableRows[1:] 194 treeTableHeight = 1 + len(treeTableRows) 195 } 196 197 systemMessagesHeight := len(systemMessages) 198 if len(systemMessages) > 0 { 199 systemMessagesHeight += 3 // Account for padding + title 200 } 201 202 // Layout the display. The extra '1' accounts for the fact that we terminate each line with a newline. 203 totalHeight := treeTableHeight + systemMessagesHeight + 1 204 r.maxTreeTableOffset = 0 205 206 // If this is not the final frame and the terminal is not large enough to show the entire display: 207 // - If there are no system messages, devote the entire display to the tree table 208 // - If there are system messages, devote the first two thirds of the display to the tree table and the 209 // last third to the system messages 210 var treeTableFooter string 211 if !done && totalHeight >= termHeight { 212 if systemMessagesHeight > 0 { 213 systemMessagesHeight = termHeight / 3 214 if systemMessagesHeight <= 3 { 215 systemMessagesHeight = 0 216 } else { 217 systemMessagesContentHeight := systemMessagesHeight - 3 218 if len(systemMessages) > systemMessagesContentHeight { 219 systemMessages = systemMessages[len(systemMessages)-systemMessagesContentHeight:] 220 } 221 } 222 } 223 224 treeTableHeight = termHeight - systemMessagesHeight - 1 225 r.maxTreeTableOffset = len(treeTableRows) - treeTableHeight - 1 226 227 treeTableRows = treeTableRows[r.treeTableOffset : r.treeTableOffset+treeTableHeight-1] 228 229 totalHeight = treeTableHeight + systemMessagesHeight + 1 230 231 upArrow := " " 232 if r.treeTableOffset != 0 { 233 upArrow = "⬆ " 234 } 235 downArrow := " " 236 if r.treeTableOffset != r.maxTreeTableOffset { 237 downArrow = "⬇ " 238 } 239 footer := fmt.Sprintf("%smore%s", upArrow, downArrow) 240 padding := termWidth - uniseg.GraphemeClusterCount(footer) 241 treeTableFooter = strings.Repeat(" ", padding) + footer 242 } 243 244 // Re-home the cursor. 245 r.term.ClearLine() 246 for ; r.rewind > 0; r.rewind-- { 247 r.term.CursorUp(1) 248 r.term.ClearLine() 249 } 250 r.rewind = totalHeight - 1 251 252 // Render the tree table. 253 r.println(nil, r.clampLine(treeTableHeader, termWidth)) 254 for _, row := range treeTableRows { 255 r.println(nil, r.clampLine(row, termWidth)) 256 } 257 if treeTableFooter != "" { 258 r.print(treeTableFooter) 259 } 260 261 // Render the system messages. 262 if systemMessagesHeight > 0 { 263 r.println(nil, "") 264 r.println(nil, colors.Yellow+"System Messages"+colors.Reset) 265 266 for _, line := range systemMessages { 267 r.println(nil, " "+line) 268 } 269 } 270 271 if done && totalHeight > 0 { 272 r.println(nil, "") 273 } 274 } 275 276 func (r *treeRenderer) clampLine(line string, maxWidth int) string { 277 // Ensure we don't go past the end of the terminal. Note: this is made complex due to 278 // msgWithColors having the color code information embedded with it. So we need to get 279 // the right substring of it, assuming that embedded colors are just markup and do not 280 // actually contribute to the length 281 maxRowLength := maxWidth - 1 282 if maxRowLength < 0 { 283 maxRowLength = 0 284 } 285 return colors.TrimColorizedString(line, maxRowLength) 286 } 287 288 func (r *treeRenderer) handleEvents() { 289 for { 290 select { 291 case <-r.ticker.C: 292 r.frame(false, false) 293 case key := <-r.keys: 294 switch key { 295 case "ctrl+c": 296 sigint() 297 case "up": 298 if r.treeTableOffset > 0 { 299 r.treeTableOffset-- 300 } 301 r.markDirty() 302 case "down": 303 if r.treeTableOffset < r.maxTreeTableOffset { 304 r.treeTableOffset++ 305 } 306 r.markDirty() 307 } 308 case <-r.closed: 309 return 310 } 311 } 312 } 313 314 func (r *treeRenderer) pollInput() { 315 for { 316 key, err := r.term.ReadKey() 317 if err == nil { 318 r.keys <- key 319 } else if errors.Is(err, io.EOF) { 320 close(r.keys) 321 return 322 } 323 } 324 }