1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
|
package cli
import "sync"
// Buffer size of the input channel. The value is chosen for no particular
// reason.
const inputChSize = 128
// A generic main loop manager.
type loop struct {
inputCh chan event
handleCb handleCb
redrawCb redrawCb
redrawCh chan struct{}
redrawFull bool
redrawMutex *sync.Mutex
returnCh chan loopReturn
}
type loopReturn struct {
buffer string
err error
}
// A placeholder type for events.
type event any
// Callback for redrawing the editor UI to the terminal.
type redrawCb func(flag redrawFlag)
func dummyRedrawCb(redrawFlag) {}
// Flag to redrawCb.
type redrawFlag uint
// Bit flags for redrawFlag.
const (
// fullRedraw signals a "full redraw". This is set on the first RedrawCb
// call or when Redraw has been called with full = true.
fullRedraw redrawFlag = 1 << iota
// finalRedraw signals that this is the final redraw in the event loop.
finalRedraw
)
// Callback for handling a terminal event.
type handleCb func(event)
func dummyHandleCb(event) {}
// newLoop creates a new Loop instance.
func newLoop() *loop {
return &loop{
inputCh: make(chan event, inputChSize),
handleCb: dummyHandleCb,
redrawCb: dummyRedrawCb,
redrawCh: make(chan struct{}, 1),
redrawFull: false,
redrawMutex: new(sync.Mutex),
returnCh: make(chan loopReturn, 1),
}
}
// HandleCb sets the handle callback. It must be called before any Read call.
func (lp *loop) HandleCb(cb handleCb) {
lp.handleCb = cb
}
// RedrawCb sets the redraw callback. It must be called before any Read call.
func (lp *loop) RedrawCb(cb redrawCb) {
lp.redrawCb = cb
}
// Redraw requests a redraw. If full is true, a full redraw is requested. It
// never blocks.
func (lp *loop) Redraw(full bool) {
lp.redrawMutex.Lock()
defer lp.redrawMutex.Unlock()
if full {
lp.redrawFull = true
}
select {
case lp.redrawCh <- struct{}{}:
default:
}
}
// Input provides an input event. It may block if the internal event buffer is
// full.
func (lp *loop) Input(ev event) {
lp.inputCh <- ev
}
// Return requests the main loop to return. It never blocks. If Return has been
// called before during the current loop iteration, it has no effect.
func (lp *loop) Return(buffer string, err error) {
select {
case lp.returnCh <- loopReturn{buffer, err}:
default:
}
}
// HasReturned returns whether Return has been called during the current loop
// iteration.
func (lp *loop) HasReturned() bool {
return len(lp.returnCh) == 1
}
// Run runs the event loop, until the Return method is called. It is generic
// and delegates all concrete work to callbacks. It is fully serial: it does
// not spawn any goroutines and never calls two callbacks in parallel, so the
// callbacks may manipulate shared states without synchronization.
func (lp *loop) Run() (buffer string, err error) {
for {
var flag redrawFlag
if lp.extractRedrawFull() {
flag |= fullRedraw
}
lp.redrawCb(flag)
select {
case event := <-lp.inputCh:
// Consume all events in the channel to minimize redraws.
consumeAllEvents:
for {
lp.handleCb(event)
select {
case ret := <-lp.returnCh:
lp.redrawCb(finalRedraw)
return ret.buffer, ret.err
default:
}
select {
case event = <-lp.inputCh:
// Continue the loop of consuming all events.
default:
break consumeAllEvents
}
}
case ret := <-lp.returnCh:
lp.redrawCb(finalRedraw)
return ret.buffer, ret.err
case <-lp.redrawCh:
}
}
}
func (lp *loop) extractRedrawFull() bool {
lp.redrawMutex.Lock()
defer lp.redrawMutex.Unlock()
full := lp.redrawFull
lp.redrawFull = false
return full
}
|