github.com/hazelops/ize@v1.1.12-0.20230915191306-97d7c0e48f11/pkg/terminal/glint_step_group.go (about) 1 package terminal 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "sync" 8 "time" 9 10 "github.com/mitchellh/go-glint" 11 ) 12 13 // glintStepGroup implements StepGroup with live updating and a display 14 // "window" for live terminal output (when using TermOutput). 15 type glintStepGroup struct { 16 mu sync.Mutex 17 ctx context.Context 18 cancel context.CancelFunc 19 wg sync.WaitGroup 20 steps []*glintStep 21 closed bool 22 } 23 24 // Start a step in the output 25 func (f *glintStepGroup) Add(str string, args ...interface{}) Step { 26 f.mu.Lock() 27 defer f.mu.Unlock() 28 29 // Build our step 30 step := &glintStep{ctx: f.ctx, status: newGlintStatus()} 31 32 // Setup initial status 33 step.Update(str, args...) 34 35 // If we're closed we don't add this step to our waitgroup or document. 36 // We still create a step and return a non-nil step so downstreams don't 37 // crash. 38 if !f.closed { 39 // Add since we have a step 40 step.wg = &f.wg 41 f.wg.Add(1) 42 43 // Add it to our list 44 f.steps = append(f.steps, step) 45 } 46 47 return step 48 } 49 50 func (f *glintStepGroup) Wait() { 51 f.mu.Lock() 52 f.closed = true 53 f.cancel() 54 wg := &f.wg 55 f.mu.Unlock() 56 57 wg.Wait() 58 } 59 60 func (f *glintStepGroup) Body(context.Context) glint.Component { 61 f.mu.Lock() 62 defer f.mu.Unlock() 63 64 var cs []glint.Component 65 for _, s := range f.steps { 66 cs = append(cs, s) 67 } 68 69 return glint.Fragment(cs...) 70 } 71 72 type glintStep struct { 73 mu sync.Mutex 74 ctx context.Context 75 wg *sync.WaitGroup 76 done bool 77 msg string 78 statusVal string 79 status *glintStatus 80 term *glintTerm 81 } 82 83 func (f *glintStep) TermOutput() io.Writer { 84 f.mu.Lock() 85 defer f.mu.Unlock() 86 87 if f.term == nil { 88 t, err := newGlintTerm(f.ctx, 10, 80) 89 if err != nil { 90 panic(err) 91 } 92 93 f.term = t 94 } 95 96 return f.term 97 } 98 99 func (f *glintStep) Update(str string, args ...interface{}) { 100 f.mu.Lock() 101 defer f.mu.Unlock() 102 f.msg = fmt.Sprintf(str, args...) 103 f.status.reset() 104 105 if f.statusVal != "" { 106 f.status.Step(f.statusVal, f.msg) 107 } else { 108 f.status.Update(f.msg) 109 } 110 } 111 112 func (f *glintStep) Status(status string) { 113 f.mu.Lock() 114 defer f.mu.Unlock() 115 f.statusVal = status 116 f.status.reset() 117 f.status.Step(status, f.msg) 118 } 119 120 func (f *glintStep) Done() { 121 f.mu.Lock() 122 defer f.mu.Unlock() 123 124 if f.done { 125 return 126 } 127 128 // Set done 129 f.done = true 130 131 // Set status 132 if f.statusVal == "" { 133 f.status.reset() 134 f.status.Step(StatusOK, f.msg) 135 } 136 137 // Unset the waitgroup 138 f.wg.Done() 139 time.Sleep(f.minimumLag()) 140 } 141 142 func (f *glintStep) Abort() { 143 f.mu.Lock() 144 defer f.mu.Unlock() 145 146 if f.done { 147 return 148 } 149 150 f.done = true 151 152 // This will cause the term to render the full scrollback from now on 153 if f.term != nil { 154 f.term.showFull() 155 } 156 157 f.status.Step(StatusError, f.msg) 158 f.wg.Done() 159 time.Sleep(f.minimumLag()) 160 } 161 162 func (f *glintStep) Body(context.Context) glint.Component { 163 f.mu.Lock() 164 defer f.mu.Unlock() 165 166 var cs []glint.Component 167 cs = append(cs, f.status) 168 169 // If we have a terminal, output that too. 170 if f.term != nil { 171 cs = append(cs, f.term) 172 } 173 174 return glint.Fragment(cs...) 175 } 176 177 func (s *glintStep) minimumLag() time.Duration { 178 return time.Millisecond * 50 179 }