github.com/xyproto/u-root@v6.0.1-0.20200302025726-5528e0c77a3c+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 }