gitlab.com/apertussolutions/u-root@v7.0.0+incompatible/cmds/core/elvish/edit/prompt/prompt.go (about)

     1  // Package prompt implements the prompt subsystem of the editor.
     2  package prompt
     3  
     4  import (
     5  	"io/ioutil"
     6  	"math"
     7  	"os"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/u-root/u-root/cmds/core/elvish/edit/eddefs"
    12  	"github.com/u-root/u-root/cmds/core/elvish/edit/ui"
    13  	"github.com/u-root/u-root/cmds/core/elvish/eval"
    14  	"github.com/u-root/u-root/cmds/core/elvish/eval/vals"
    15  	"github.com/u-root/u-root/cmds/core/elvish/eval/vars"
    16  	"github.com/u-root/u-root/cmds/core/elvish/util"
    17  )
    18  
    19  var logger = util.GetLogger("[edit/prompt] ")
    20  
    21  // Init initializes the prompt subsystem of the editor.
    22  func Init(ed eddefs.Editor, ns eval.Ns) {
    23  	prompt := makePrompt(ed, defaultPrompt)
    24  	rprompt := makePrompt(ed, defaultRPrompt)
    25  	ed.SetPrompt(prompt)
    26  	ed.SetRPrompt(rprompt)
    27  	installAPI(ns, prompt, "prompt")
    28  	installAPI(ns, rprompt, "rprompt")
    29  }
    30  
    31  func installAPI(ns eval.Ns, p *prompt, basename string) {
    32  	ns.Add(basename, vars.FromPtr(&p.fn))
    33  	ns.Add(basename+"-stale-threshold", vars.FromPtr(&p.staleThreshold))
    34  	ns.Add(basename+"-stale-transform", vars.FromPtr(&p.staleTransform))
    35  	ns.Add("-"+basename+"-eagerness", vars.FromPtr(&p.eagerness))
    36  }
    37  
    38  type prompt struct {
    39  	ed eddefs.Editor
    40  	// The main callback.
    41  	fn eval.Callable
    42  	// Callback used to transform stale prompts.
    43  	staleTransform eval.Callable
    44  	// Threshold in seconds for a prompt to be considered as stale.
    45  	staleThreshold float64
    46  	// How eager the prompt should be updated. When >= 5, updated when directory
    47  	// is changed. When >= 10, always update. Default is 5.
    48  	eagerness int
    49  
    50  	// Working directory when prompt was last updated.
    51  	lastWd string
    52  	// Channel for update requests.
    53  	updateReq chan struct{}
    54  	// Channel on which prompt contents are sent.
    55  	ch chan []*ui.Styled
    56  	// Last prompt content
    57  	last      []*ui.Styled
    58  	lastMutex *sync.RWMutex
    59  }
    60  
    61  var unknownContent = []*ui.Styled{{"???> ", ui.Styles{}}}
    62  
    63  func makePrompt(ed eddefs.Editor, fn eval.Callable) *prompt {
    64  	p := &prompt{
    65  		ed, fn, defaultStaleTransform, 0.2, 5,
    66  		"", make(chan struct{}, 1), make(chan []*ui.Styled, 1),
    67  		unknownContent, new(sync.RWMutex)}
    68  	go p.loop()
    69  	return p
    70  }
    71  
    72  func (p *prompt) loop() {
    73  	content := unknownContent
    74  	ch := make(chan []*ui.Styled)
    75  	for range p.updateReq {
    76  		go func() {
    77  			ch <- callPrompt(p.ed, p.fn)
    78  		}()
    79  
    80  		select {
    81  		case <-makeMaxWaitChan(p.staleThreshold):
    82  			// The prompt callback did not finish within the threshold. Send the
    83  			// previous content, marked as stale.
    84  			p.send(callTransformer(p.ed, p.staleTransform, content))
    85  			content = <-ch
    86  
    87  			select {
    88  			case <-p.updateReq:
    89  				// If another update is already requested by the time we finish,
    90  				// keep marking the prompt as stale. This reduces flickering.
    91  				p.send(callTransformer(p.ed, p.staleTransform, content))
    92  				p.queueUpdate()
    93  			default:
    94  				p.send(content)
    95  			}
    96  		case content = <-ch:
    97  			p.send(content)
    98  		}
    99  	}
   100  }
   101  
   102  func (p *prompt) Chan() <-chan []*ui.Styled {
   103  	return p.ch
   104  }
   105  
   106  func (p *prompt) Update(force bool) {
   107  	if force || p.shouldUpdate() {
   108  		p.queueUpdate()
   109  	}
   110  }
   111  
   112  func (p *prompt) Last() []*ui.Styled {
   113  	p.lastMutex.RLock()
   114  	defer p.lastMutex.RUnlock()
   115  	return p.last
   116  }
   117  
   118  func (p *prompt) Close() error {
   119  	// TODO: Close p.updateReq. However, doing this can cause
   120  	// write-to-closed-channel panics.
   121  	return nil
   122  }
   123  
   124  func (p *prompt) queueUpdate() {
   125  	select {
   126  	case p.updateReq <- struct{}{}:
   127  	default:
   128  	}
   129  }
   130  
   131  func (p *prompt) send(content []*ui.Styled) {
   132  	p.lastMutex.Lock()
   133  	p.last = content
   134  	p.lastMutex.Unlock()
   135  	p.ch <- content
   136  }
   137  
   138  func (p *prompt) shouldUpdate() bool {
   139  	if p.eagerness >= 10 {
   140  		return true
   141  	}
   142  	if p.eagerness >= 5 {
   143  		wd, err := os.Getwd()
   144  		if err != nil {
   145  			wd = "error"
   146  		}
   147  		oldWd := p.lastWd
   148  		p.lastWd = wd
   149  		return wd != oldWd
   150  	}
   151  	return false
   152  }
   153  
   154  // maxSeconds is the maximum number of seconds time.Duration can represent.
   155  const maxSeconds = float64(math.MaxInt64 / time.Second)
   156  
   157  // makeMaxWaitChan makes a channel that sends the current time after f seconds.
   158  // If f does not fits in a time.Duration value, it returns nil, which is a
   159  // channel that never sends any value.
   160  func makeMaxWaitChan(f float64) <-chan time.Time {
   161  	if f > maxSeconds {
   162  		return nil
   163  	}
   164  	return time.After(time.Duration(f * float64(time.Second)))
   165  }
   166  
   167  // callPrompt calls a function with no arguments and closed input, and converts
   168  // its outputs to styled objects. Used to call prompt callbacks.
   169  func callPrompt(ed eddefs.Editor, fn eval.Callable) []*ui.Styled {
   170  	ports := []*eval.Port{
   171  		eval.DevNullClosedChan,
   172  		{}, // Will be replaced when capturing output
   173  		{File: os.Stderr},
   174  	}
   175  
   176  	return callAndGetStyled(ed, fn, ports)
   177  }
   178  
   179  // callTransformer calls a function with no arguments and the given inputs, and
   180  // converts its outputs to styled objects. Used to call stale transformers.
   181  func callTransformer(ed eddefs.Editor, fn eval.Callable, currentPrompt []*ui.Styled) []*ui.Styled {
   182  	input := make(chan interface{})
   183  	stopInputWriter := make(chan struct{})
   184  
   185  	ports := []*eval.Port{
   186  		{Chan: input, File: eval.DevNull},
   187  		{}, // Will be replaced when capturing output
   188  		{File: os.Stderr},
   189  	}
   190  	go func() {
   191  		defer close(input)
   192  		for _, char := range currentPrompt {
   193  			select {
   194  			case input <- char:
   195  			case <-stopInputWriter:
   196  				return
   197  			}
   198  		}
   199  	}()
   200  	defer close(stopInputWriter)
   201  
   202  	return callAndGetStyled(ed, fn, ports)
   203  }
   204  
   205  func callAndGetStyled(ed eddefs.Editor, fn eval.Callable, ports []*eval.Port) []*ui.Styled {
   206  	var (
   207  		styleds      []*ui.Styled
   208  		styledsMutex sync.Mutex
   209  	)
   210  	add := func(s *ui.Styled) {
   211  		styledsMutex.Lock()
   212  		styleds = append(styleds, s)
   213  		styledsMutex.Unlock()
   214  	}
   215  	// Value output may be of type ui.Styled or any other type, in which case
   216  	// they are converted to ui.Styled.
   217  	valuesCb := func(ch <-chan interface{}) {
   218  		for v := range ch {
   219  			if s, ok := v.(*ui.Styled); ok {
   220  				add(s)
   221  			} else {
   222  				add(&ui.Styled{vals.ToString(v), ui.Styles{}})
   223  			}
   224  		}
   225  	}
   226  	// Byte output is added to the prompt as a single unstyled text.
   227  	bytesCb := func(r *os.File) {
   228  		allBytes, err := ioutil.ReadAll(r)
   229  		if err != nil {
   230  			logger.Println("error reading prompt byte output:", err)
   231  		}
   232  		if len(allBytes) > 0 {
   233  			add(&ui.Styled{string(allBytes), ui.Styles{}})
   234  		}
   235  	}
   236  
   237  	// XXX There is no source to pass to NewTopEvalCtx.
   238  	ec := eval.NewTopFrame(ed.Evaler(), eval.NewInternalSource("[prompt]"), ports)
   239  	err := ec.CallWithOutputCallback(fn, nil, eval.NoOpts, valuesCb, bytesCb)
   240  
   241  	if err != nil {
   242  		ed.Notify("prompt function error: %v", err)
   243  		return nil
   244  	}
   245  
   246  	return styleds
   247  }