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 }