github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/cli/prompt/prompt.go (about)

     1  // Package prompt provides an implementation of the cli.Prompt interface.
     2  package prompt
     3  
     4  import (
     5  	"os"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/markusbkk/elvish/pkg/ui"
    10  )
    11  
    12  // Prompt implements a prompt that is executed asynchronously.
    13  type Prompt struct {
    14  	config Config
    15  
    16  	// Working directory when prompt was last updated.
    17  	lastWd string
    18  	// Channel for update requests.
    19  	updateReq chan struct{}
    20  	// Channel on which prompt contents are delivered.
    21  	ch chan struct{}
    22  	// Last computed prompt content.
    23  	last ui.Text
    24  	// Mutex for guarding access to the last field.
    25  	lastMutex sync.RWMutex
    26  }
    27  
    28  // Config keeps configurations for the prompt.
    29  type Config struct {
    30  	// The function that computes the prompt.
    31  	Compute func() ui.Text
    32  	// Function to transform stale prompts.
    33  	StaleTransform func(ui.Text) ui.Text
    34  	// Threshold for a prompt to be considered as stale.
    35  	StaleThreshold func() time.Duration
    36  	// How eager the prompt should be updated. When >= 5, updated when directory
    37  	// is changed. When >= 10, always update. Default is 5.
    38  	Eagerness func() int
    39  }
    40  
    41  func defaultStaleTransform(t ui.Text) ui.Text {
    42  	return ui.StyleText(t, ui.Inverse)
    43  }
    44  
    45  const defaultStaleThreshold = 200 * time.Millisecond
    46  
    47  const defaultEagerness = 5
    48  
    49  var unknownContent = ui.T("???> ")
    50  
    51  // New makes a new prompt.
    52  func New(cfg Config) *Prompt {
    53  	if cfg.Compute == nil {
    54  		cfg.Compute = func() ui.Text { return unknownContent }
    55  	}
    56  	if cfg.StaleTransform == nil {
    57  		cfg.StaleTransform = defaultStaleTransform
    58  	}
    59  	if cfg.StaleThreshold == nil {
    60  		cfg.StaleThreshold = func() time.Duration { return defaultStaleThreshold }
    61  	}
    62  	if cfg.Eagerness == nil {
    63  		cfg.Eagerness = func() int { return defaultEagerness }
    64  	}
    65  	p := &Prompt{
    66  		cfg,
    67  		"", make(chan struct{}, 1), make(chan struct{}, 1),
    68  		unknownContent, sync.RWMutex{}}
    69  	// TODO: Don't keep a goroutine running.
    70  	go p.loop()
    71  	return p
    72  }
    73  
    74  func (p *Prompt) loop() {
    75  	content := unknownContent
    76  	ch := make(chan ui.Text)
    77  	for range p.updateReq {
    78  		go func() {
    79  			ch <- p.config.Compute()
    80  		}()
    81  
    82  		select {
    83  		case <-time.After(p.config.StaleThreshold()):
    84  			// The prompt callback did not finish within the threshold. Send the
    85  			// previous content, marked as stale.
    86  			p.update(p.config.StaleTransform(content))
    87  			content = <-ch
    88  
    89  			select {
    90  			case <-p.updateReq:
    91  				// If another update is already requested by the time we finish,
    92  				// keep marking the prompt as stale. This reduces flickering.
    93  				p.update(p.config.StaleTransform(content))
    94  				p.queueUpdate()
    95  			default:
    96  				p.update(content)
    97  			}
    98  		case content = <-ch:
    99  			p.update(content)
   100  		}
   101  	}
   102  }
   103  
   104  // Trigger triggers an update to the prompt.
   105  func (p *Prompt) Trigger(force bool) {
   106  	if force || p.shouldUpdate() {
   107  		p.queueUpdate()
   108  	}
   109  }
   110  
   111  // Get returns the current content of the prompt.
   112  func (p *Prompt) Get() ui.Text {
   113  	p.lastMutex.RLock()
   114  	defer p.lastMutex.RUnlock()
   115  	return p.last
   116  }
   117  
   118  // LateUpdates returns a channel on which late updates are made available.
   119  func (p *Prompt) LateUpdates() <-chan struct{} {
   120  	return p.ch
   121  }
   122  
   123  func (p *Prompt) queueUpdate() {
   124  	select {
   125  	case p.updateReq <- struct{}{}:
   126  	default:
   127  	}
   128  }
   129  
   130  func (p *Prompt) update(content ui.Text) {
   131  	p.lastMutex.Lock()
   132  	p.last = content
   133  	p.lastMutex.Unlock()
   134  	p.ch <- struct{}{}
   135  }
   136  
   137  func (p *Prompt) shouldUpdate() bool {
   138  	eagerness := p.config.Eagerness()
   139  	if eagerness >= 10 {
   140  		return true
   141  	}
   142  	if 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  }