prompt.go 3.9 KB

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