src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/edit/prompt.go (about) 1 package edit 2 3 import ( 4 "io" 5 "os" 6 "os/user" 7 "sync" 8 "time" 9 10 "src.elv.sh/pkg/cli" 11 "src.elv.sh/pkg/cli/prompt" 12 "src.elv.sh/pkg/eval" 13 "src.elv.sh/pkg/eval/vals" 14 "src.elv.sh/pkg/eval/vars" 15 "src.elv.sh/pkg/fsutil" 16 "src.elv.sh/pkg/ui" 17 ) 18 19 func initPrompts(appSpec *cli.AppSpec, nt notifier, ev *eval.Evaler, nb eval.NsBuilder) { 20 promptVal, rpromptVal := getDefaultPromptVals() 21 initPrompt(&appSpec.Prompt, "prompt", promptVal, nt, ev, nb) 22 initPrompt(&appSpec.RPrompt, "rprompt", rpromptVal, nt, ev, nb) 23 24 rpromptPersistentVar := newBoolVar(false) 25 appSpec.RPromptPersistent = func() bool { return rpromptPersistentVar.Get().(bool) } 26 nb.AddVar("rprompt-persistent", rpromptPersistentVar) 27 } 28 29 func initPrompt(p *cli.Prompt, name string, val eval.Callable, nt notifier, ev *eval.Evaler, nb eval.NsBuilder) { 30 computeVar := vars.FromPtr(&val) 31 nb.AddVar(name, computeVar) 32 eagernessVar := newIntVar(5) 33 nb.AddVar("-"+name+"-eagerness", eagernessVar) 34 staleThresholdVar := newFloatVar(0.2) 35 nb.AddVar(name+"-stale-threshold", staleThresholdVar) 36 staleTransformVar := newFnVar( 37 eval.NewGoFn("<default stale transform>", defaultStaleTransform)) 38 nb.AddVar(name+"-stale-transform", staleTransformVar) 39 40 *p = prompt.New(prompt.Config{ 41 Compute: func() ui.Text { 42 return callForStyledText(nt, ev, name, computeVar.Get().(eval.Callable)) 43 }, 44 Eagerness: func() int { return eagernessVar.GetRaw().(int) }, 45 StaleThreshold: func() time.Duration { 46 seconds := staleThresholdVar.GetRaw().(float64) 47 return time.Duration(seconds * float64(time.Second)) 48 }, 49 StaleTransform: func(original ui.Text) ui.Text { 50 return callForStyledText(nt, ev, name+" stale transform", staleTransformVar.Get().(eval.Callable), original) 51 }, 52 }) 53 } 54 55 func getDefaultPromptVals() (prompt, rprompt eval.Callable) { 56 user, userErr := user.Current() 57 isRoot := userErr == nil && user.Uid == "0" 58 59 username := "???" 60 if userErr == nil { 61 username = user.Username 62 } 63 hostname, err := os.Hostname() 64 if err != nil { 65 hostname = "???" 66 } 67 68 return getDefaultPrompt(isRoot), getDefaultRPrompt(username, hostname) 69 } 70 71 func getDefaultPrompt(isRoot bool) eval.Callable { 72 p := ui.T("> ") 73 if isRoot { 74 p = ui.T("# ", ui.FgRed) 75 } 76 return eval.NewGoFn("default prompt", func() ui.Text { 77 return ui.Concat(ui.T(fsutil.Getwd()), p) 78 }) 79 } 80 81 func getDefaultRPrompt(username, hostname string) eval.Callable { 82 rp := ui.T(username+"@"+hostname, ui.Inverse) 83 return eval.NewGoFn("default rprompt", func() ui.Text { 84 return rp 85 }) 86 } 87 88 func defaultStaleTransform(original ui.Text) ui.Text { 89 return ui.StyleText(original, ui.Inverse) 90 } 91 92 // Calls a function with the given arguments and closed input, and concatenates 93 // its outputs to a styled text. Used to call prompts and stale transformers. 94 func callForStyledText(nt notifier, ev *eval.Evaler, ctx string, fn eval.Callable, args ...any) ui.Text { 95 var ( 96 result ui.Text 97 resultMutex sync.Mutex 98 ) 99 add := func(v any) { 100 resultMutex.Lock() 101 defer resultMutex.Unlock() 102 newResult, err := result.Concat(v) 103 if err != nil { 104 nt.notifyf("invalid output type from prompt: %s", vals.Kind(v)) 105 } else { 106 result = newResult.(ui.Text) 107 } 108 } 109 110 // Value outputs are concatenated. 111 valuesCb := func(ch <-chan any) { 112 for v := range ch { 113 add(v) 114 } 115 } 116 // Byte output is added to the prompt as a single unstyled text. 117 bytesCb := func(r *os.File) { 118 allBytes, err := io.ReadAll(r) 119 if err != nil { 120 nt.notifyf("error reading prompt byte output: %v", err) 121 } 122 if len(allBytes) > 0 { 123 add(ui.ParseSGREscapedText(string(allBytes))) 124 } 125 } 126 127 port1, done1, err := eval.PipePort(valuesCb, bytesCb) 128 if err != nil { 129 nt.notifyf("cannot create pipe for prompt: %v", err) 130 return nil 131 } 132 port2, done2 := makeNotifyPort(nt) 133 134 err = ev.Call(fn, 135 eval.CallCfg{Args: args, From: "[" + ctx + "]"}, 136 eval.EvalCfg{Ports: []*eval.Port{nil, port1, port2}}) 137 done1() 138 done2() 139 140 if err != nil { 141 nt.notifyError(ctx, err) 142 } 143 return result 144 }