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  }