src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/cli/loop.go (about)

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