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

     1  package modes
     2  
     3  import (
     4  	"errors"
     5  
     6  	"src.elv.sh/pkg/cli"
     7  	"src.elv.sh/pkg/cli/term"
     8  	"src.elv.sh/pkg/cli/tk"
     9  	"src.elv.sh/pkg/ui"
    10  )
    11  
    12  // Instant is a mode that executes code whenever it changes and shows the
    13  // result.
    14  type Instant interface {
    15  	tk.Widget
    16  }
    17  
    18  // InstantSpec specifies the configuration for the instant mode.
    19  type InstantSpec struct {
    20  	// Key bindings.
    21  	Bindings tk.Bindings
    22  	// The function to execute code and returns the output.
    23  	Execute func(code string) ([]string, error)
    24  }
    25  
    26  type instant struct {
    27  	InstantSpec
    28  	attachedTo tk.CodeArea
    29  	textView   tk.TextView
    30  	lastCode   string
    31  	lastErr    error
    32  }
    33  
    34  func (w *instant) Render(width, height int) *term.Buffer {
    35  	buf := w.render(width, height)
    36  	buf.TrimToLines(0, height)
    37  	return buf
    38  }
    39  
    40  func (w *instant) MaxHeight(width, height int) int {
    41  	return len(w.render(width, height).Lines)
    42  }
    43  
    44  func (w *instant) render(width, height int) *term.Buffer {
    45  	bb := term.NewBufferBuilder(width).
    46  		WriteStyled(modeLine(" INSTANT ", false)).SetDotHere()
    47  	if w.lastErr != nil {
    48  		bb.Newline().Write(w.lastErr.Error(), ui.FgRed)
    49  	}
    50  	buf := bb.Buffer()
    51  	if len(buf.Lines) < height {
    52  		bufTextView := w.textView.Render(width, height-len(buf.Lines))
    53  		buf.Extend(bufTextView, false)
    54  	}
    55  	return buf
    56  }
    57  
    58  func (w *instant) Focus() bool { return false }
    59  
    60  func (w *instant) Handle(event term.Event) bool {
    61  	handled := w.Bindings.Handle(w, event)
    62  	if !handled {
    63  		handled = w.attachedTo.Handle(event)
    64  	}
    65  	w.update(false)
    66  	return handled
    67  }
    68  
    69  func (w *instant) update(force bool) {
    70  	code := w.attachedTo.CopyState().Buffer.Content
    71  	if code == w.lastCode && !force {
    72  		return
    73  	}
    74  	w.lastCode = code
    75  	output, err := w.Execute(code)
    76  	w.lastErr = err
    77  	if err == nil {
    78  		w.textView.MutateState(func(s *tk.TextViewState) {
    79  			*s = tk.TextViewState{Lines: output, First: 0}
    80  		})
    81  	}
    82  }
    83  
    84  var errExecutorIsRequired = errors.New("executor is required")
    85  
    86  // NewInstant creates a new instant mode.
    87  func NewInstant(app cli.App, cfg InstantSpec) (Instant, error) {
    88  	codeArea, err := FocusedCodeArea(app)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  	if cfg.Execute == nil {
    93  		return nil, errExecutorIsRequired
    94  	}
    95  	if cfg.Bindings == nil {
    96  		cfg.Bindings = tk.DummyBindings{}
    97  	}
    98  	w := instant{
    99  		InstantSpec: cfg,
   100  		attachedTo:  codeArea,
   101  		textView:    tk.NewTextView(tk.TextViewSpec{Scrollable: true}),
   102  	}
   103  	w.update(true)
   104  	return &w, nil
   105  }