github.com/hazelops/ize@v1.1.12-0.20230915191306-97d7c0e48f11/pkg/terminal/display.go (about) 1 package terminal 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "os" 8 "strings" 9 "sync" 10 "time" 11 12 "github.com/briandowns/spinner" 13 "github.com/containerd/console" 14 "github.com/lab47/vterm/parser" 15 "github.com/lab47/vterm/screen" 16 "github.com/lab47/vterm/state" 17 "github.com/morikuni/aec" 18 ) 19 20 var spinnerSet = spinner.CharSets[11] 21 22 type DisplayEntry struct { 23 d *Display 24 line uint 25 index int 26 indent int 27 spinner bool 28 text string 29 status string 30 31 body []string 32 33 next *DisplayEntry 34 } 35 36 type Display struct { 37 mu sync.Mutex 38 Entries []*DisplayEntry 39 40 w io.Writer 41 newEnt chan *DisplayEntry 42 updates chan *DisplayEntry 43 resize chan struct{} // sent to when an entry has resized itself. 44 line uint 45 width int 46 47 wg sync.WaitGroup 48 spinning int 49 } 50 51 func NewDisplay(ctx context.Context, w io.Writer) *Display { 52 d := &Display{ 53 w: w, 54 width: 80, 55 updates: make(chan *DisplayEntry), 56 resize: make(chan struct{}), 57 newEnt: make(chan *DisplayEntry), 58 } 59 60 if f, ok := w.(*os.File); ok { 61 if c, err := console.ConsoleFromFile(f); err == nil { 62 if sz, err := c.Size(); err == nil { 63 if sz.Width >= 10 { 64 d.width = int(sz.Width) - 1 65 } 66 } 67 } 68 } 69 70 d.wg.Add(1) 71 go func() { 72 defer d.wg.Done() 73 d.Display(ctx) 74 }() 75 76 return d 77 } 78 79 func (d *Display) Close() error { 80 d.wg.Wait() 81 return nil 82 } 83 84 func (d *Display) flushAll() { 85 d.mu.Lock() 86 defer d.mu.Unlock() 87 88 for range d.Entries { 89 fmt.Fprintln(d.w, "") 90 } 91 92 d.line = uint(len(d.Entries)) 93 } 94 95 func (d *Display) renderEntry(ent *DisplayEntry, spin int) { 96 b := aec.EmptyBuilder 97 98 diff := d.line - ent.line 99 100 text := strings.TrimRight(ent.text, " \t\n") 101 102 if len(text) >= d.width { 103 text = text[:d.width-1] 104 } 105 106 prefix := "" 107 if ent.spinner { 108 prefix = spinnerSet[spin] + " " 109 } 110 111 var statusColor *aec.Builder 112 if ent.status != "" { 113 icon, ok := statusIcons[ent.status] 114 if !ok { 115 icon = ent.status 116 } 117 118 if len(prefix) > 0 { 119 prefix = prefix + " " + icon + " " 120 } else { 121 prefix = icon + " " 122 } 123 124 if codes, ok := colorStatus[ent.status]; ok { 125 statusColor = b.With(codes...) 126 } 127 } 128 129 line := fmt.Sprintf("%s%s%s", 130 b. 131 Up(diff). 132 Column(0). 133 EraseLine(aec.EraseModes.All). 134 ANSI, 135 prefix, 136 text, 137 ) 138 139 if statusColor != nil { 140 line = statusColor.ANSI.Apply(line) 141 } 142 143 fmt.Fprint(d.w, line) 144 145 for _, body := range ent.body { 146 fmt.Fprintf(d.w, "%s%s", 147 b. 148 Down(1). 149 Column(0). 150 ANSI, 151 body, 152 ) 153 diff-- 154 } 155 156 fmt.Fprintf(d.w, "%s", 157 b. 158 Down(diff). 159 Column(0). 160 ANSI, 161 ) 162 } 163 164 func (d *Display) Display(ctx context.Context) { 165 // d.flushAll() 166 167 ticker := time.NewTicker(time.Second / 6) 168 169 var spin int 170 171 for { 172 select { 173 case <-ctx.Done(): 174 return 175 case <-ticker.C: 176 spin++ 177 if spin >= len(spinnerSet) { 178 spin = 0 179 } 180 181 d.mu.Lock() 182 update := d.spinning > 0 183 184 if !update { 185 d.mu.Unlock() 186 continue 187 } 188 189 for _, ent := range d.Entries { 190 if !ent.spinner { 191 continue 192 } 193 194 d.renderEntry(ent, spin) 195 } 196 197 d.mu.Unlock() 198 case ent := <-d.newEnt: 199 d.mu.Lock() 200 ent.line = d.line 201 d.Entries = append(d.Entries, ent) 202 d.line++ 203 d.line += uint(len(ent.body)) 204 fmt.Fprintln(d.w, "") 205 for i := 0; i < len(ent.body); i++ { 206 fmt.Fprintln(d.w, "") 207 } 208 209 d.mu.Unlock() 210 211 case ent := <-d.updates: 212 d.mu.Lock() 213 d.renderEntry(ent, spin) 214 d.mu.Unlock() 215 case <-d.resize: 216 d.mu.Lock() 217 218 var newLine uint 219 220 for _, ent := range d.Entries { 221 newLine++ 222 newLine += uint(len(ent.body)) 223 } 224 225 diff := newLine - d.line 226 227 // TODO should we support shrinking? 228 if diff > 0 { 229 // Pad down 230 for i := uint(0); i < diff; i++ { 231 fmt.Fprintln(d.w, "") 232 } 233 234 d.line = newLine 235 236 var cnt uint 237 238 for _, ent := range d.Entries { 239 ent.line = cnt 240 cnt++ 241 cnt += uint(len(ent.body)) 242 243 d.renderEntry(ent, spin) 244 } 245 } 246 247 d.mu.Unlock() 248 } 249 } 250 } 251 252 func (d *Display) NewStatus(indent int) *DisplayEntry { 253 de := &DisplayEntry{ 254 d: d, 255 indent: indent, 256 } 257 258 d.newEnt <- de 259 260 return de 261 } 262 263 func (d *Display) NewStatusWithBody(indent, lines int) *DisplayEntry { 264 de := &DisplayEntry{ 265 d: d, 266 indent: indent, 267 body: make([]string, lines), 268 } 269 270 d.newEnt <- de 271 272 return de 273 } 274 275 func (e *DisplayEntry) StartSpinner() { 276 e.d.mu.Lock() 277 278 e.spinner = true 279 e.d.spinning++ 280 281 e.d.mu.Unlock() 282 283 e.d.updates <- e 284 } 285 286 func (e *DisplayEntry) StopSpinner() { 287 e.d.mu.Lock() 288 289 e.spinner = false 290 e.d.spinning-- 291 292 e.d.mu.Unlock() 293 294 e.d.updates <- e 295 } 296 297 func (e *DisplayEntry) SetStatus(status string) { 298 e.d.mu.Lock() 299 defer e.d.mu.Unlock() 300 301 e.status = status 302 } 303 304 func (e *DisplayEntry) Update(str string, args ...interface{}) { 305 e.d.mu.Lock() 306 e.text = fmt.Sprintf(str, args...) 307 e.d.mu.Unlock() 308 309 e.d.updates <- e 310 } 311 312 func (e *DisplayEntry) SetBody(line int, data string) { 313 e.d.mu.Lock() 314 315 var resize bool 316 317 if line >= len(e.body) { 318 nb := make([]string, line+1) 319 320 for i, s := range e.body { 321 nb[i] = s 322 } 323 324 e.body = nb 325 resize = true 326 } 327 328 e.body[line] = data 329 e.d.mu.Unlock() 330 331 if resize { 332 e.d.resize <- struct{}{} 333 } 334 335 e.d.updates <- e 336 } 337 338 type Term struct { 339 ent *DisplayEntry 340 scr *screen.Screen 341 w io.Writer 342 ctx context.Context 343 cancel func() 344 345 output [][]rune 346 347 wg sync.WaitGroup 348 parseErr error 349 } 350 351 func (t *Term) DamageDone(r state.Rect, cr screen.CellReader) error { 352 for row := r.Start.Row; row <= r.End.Row; row++ { 353 for col := r.Start.Col; col <= r.End.Col; col++ { 354 cell := cr.GetCell(row, col) 355 356 if cell == nil { 357 t.output[row][col] = ' ' 358 } else { 359 val, _ := cell.Value() 360 361 if val == 0 { 362 t.output[row][col] = ' ' 363 } else { 364 t.output[row][col] = val 365 } 366 } 367 } 368 } 369 370 for row := r.Start.Row; row <= r.End.Row; row++ { 371 b := aec.EmptyBuilder 372 blue := b.LightBlueF() 373 t.ent.SetBody(row, fmt.Sprintf(" │ %s%s%s", blue.ANSI, string(t.output[row]), aec.Reset)) 374 } 375 376 return nil 377 } 378 379 func (t *Term) MoveCursor(p state.Pos) error { 380 // Ignore it. 381 return nil 382 } 383 384 func (t *Term) SetTermProp(attr state.TermAttr, val interface{}) error { 385 // Ignore it. 386 return nil 387 } 388 389 func (t *Term) Output(data []byte) error { 390 // Ignore it. 391 return nil 392 } 393 394 func (t *Term) StringEvent(kind string, data []byte) error { 395 // Ignore them. 396 return nil 397 } 398 399 func NewTerm(ctx context.Context, d *DisplayEntry, height, width int) (*Term, error) { 400 term := &Term{ 401 ent: d, 402 output: make([][]rune, height), 403 } 404 405 for i := range term.output { 406 term.output[i] = make([]rune, width) 407 } 408 409 scr, err := screen.NewScreen(height, width, term) 410 if err != nil { 411 return nil, err 412 } 413 414 term.scr = scr 415 416 st, err := state.NewState(height, width, scr) 417 if err != nil { 418 return nil, err 419 } 420 421 r, w, err := os.Pipe() 422 if err != nil { 423 return nil, err 424 } 425 426 term.w = w 427 428 prs, err := parser.NewParser(r, st) 429 if err != nil { 430 return nil, err 431 } 432 433 term.ctx, term.cancel = context.WithCancel(ctx) 434 435 term.wg.Add(1) 436 go func() { 437 defer term.wg.Done() 438 439 err := prs.Drive(term.ctx) 440 if err != nil && err != context.Canceled { 441 term.parseErr = err 442 } 443 }() 444 445 return term, nil 446 } 447 448 func (t *Term) Write(b []byte) (int, error) { 449 return t.w.Write(b) 450 } 451 452 func (t *Term) Close() error { 453 t.cancel() 454 t.wg.Wait() 455 return t.parseErr 456 }