package emulator import ( "errors" "fmt" "strings" "github.com/c-bata/go-prompt" "github.com/google/shlex" "edge-infra.dev/pkg/sds/lib/colors" ) // This file contains all private fields and methods for the connect prompt. // The connect prompt is responsible for: // - handling and passing connection parameters // - launching the session prompt on a successful connection // - reporting on errors from the connection // Connect generally uses the blue color scheme for its display methods i.e colors.FgBlue, colors.BgBlue, // colors.FgBlueBold and red for error. // used for returning parsed input from parseconnectinput type connectionData struct { projectID string bannerID string storeID string terminalID string } var topLevelConnectCommands = []prompt.Suggest{ {Text: "connect", Description: "Connect to a new session. See help for further info."}, {Text: "rcliconfig", Description: "Sets rcliconfig settings."}, {Text: "help", Description: "Display help"}, } var connectCommandsCompleterPhase2 = []prompt.Suggest{ {Text: "shell", Description: "Enter local shell mode to interact with remotecli exec output"}, } var rcliconfigCompleter = []prompt.Suggest{ {Text: "bannerID", Description: "Set default banner ID for workspace."}, {Text: "storeID", Description: "Set default store ID for workspace."}, } var rcliConfigCompleterPhase1 = []prompt.Suggest{ {Text: "projectID", Description: "Set default project ID for workspace."}, {Text: "enablePerSessionSubscription", Description: "Enables Creating a new Subscription per Session"}, {Text: "disablePerSessionSubscription", Description: "Disables Creating a new Subscription per Session"}, } func connectPromptCompleter(in prompt.Document) []prompt.Suggest { commands, err := shlex.Split(in.TextBeforeCursor()) if err != nil { // This will occur when there is an unclosed quote on the prompt return []prompt.Suggest{} } // Total number of words in the list on prompt lenCommands := len(commands) // Index of word currently being typed in the list, shlex.Split does not // append empty string to list when space before cursor, so must add 1 in // this case wordidx := lenCommands - 1 if in.GetWordBeforeCursor() == "" { wordidx = lenCommands } // Top level command to be run command := "" if lenCommands > 0 { command = commands[0] } if wordidx < 1 { return prompt.FilterHasPrefix(topLevelConnectCommands, command, true) } if command == "rcliconfig" && wordidx < 2 { command2 := "" if len(commands) > 1 { command2 = commands[1] } return prompt.FilterHasPrefix(rcliconfigCompleter, command2, true) } if command != "connect" { return []prompt.Suggest{} } if wordidx > 4 { return []prompt.Suggest{} } return []prompt.Suggest{} } // connect prompt generator func (em Emulator) connectPrompt(optionHistory []string) *prompt.Prompt { return prompt.New(em.connectPromptExecutor, connectPromptCompleter, prompt.OptionHistory(optionHistory), prompt.OptionSetExitCheckerOnInput(exitChecker), prompt.OptionPrefix("\r> "), prompt.OptionPrefixTextColor(prompt.Yellow), prompt.OptionPreviewSuggestionTextColor(prompt.Blue), prompt.OptionSelectedSuggestionBGColor(prompt.LightGray), prompt.OptionSuggestionBGColor(prompt.DarkGray), prompt.OptionAddKeyBind(commonKeyBindings...), ) } // handles input for the connection prompt. func (em *Emulator) connectPromptExecutor(in string) { if in == "" { return } if in == "help" { fmt.Println(connectHelp) return } for _, item := range exitCommands { if in == item { em.connectHistory.close() em.sessionHistory.close() em.shellHistory.close() return } } err := em.connectHistory.updateHistory(in, historyFileLimit) if err != nil { em.log.Error(err, "Error updating connect history file") } if strings.HasPrefix(in, "dark ") { // remove "dark " from the input string, enable darkmode option in `cliServiceItfc`, and allow standard `connect` handler to process the remaining command string in = in[5:] // enable darkmode dm, ok := em.cls.(DarkModeService) if !ok { fmt.Println(colors.Text("Error Dark mode not supported.", colors.BgRed)) return } fmt.Println(colors.BufferedText("darkmode enabled", colors.BgBrightBlack)) dm.SetDarkmode(true) // disable so the next call is not set to dark defer dm.SetDarkmode(false) } if strings.HasPrefix(in, "connect") { connectData, err := em.parseConnectInput(in) if err != nil { fmt.Println(colors.Text("error whilst parsing connection query: %q. Type help to see more info.", colors.FgRed, err)) return } err = em.cls.Connect(em.runCtx, connectData.projectID, connectData.bannerID, connectData.storeID, connectData.terminalID) if err != nil { fmt.Println(colors.Text("Error whilst setting up connection:", colors.BgRed)) em.displayError(em.log, err) return } em.startSession() fmt.Println(colors.Text("Exited session", colors.FgGreenBold)) // display the connnect prompt banner em.displayConnectBanner() return } if strings.HasPrefix(in, "shell") { connectData, err := em.parseConnectInput(in) if err != nil { fmt.Println(colors.Text("error whilst parsing connection query: %q. Type help to see more info.", colors.FgRed, err)) return } err = em.startShell(connectData) if err != nil { fmt.Println(colors.Text("Error during local shell mode initialisation:", colors.BgRed)) em.displayError(em.log, err) return } fmt.Println(colors.Text("Exited shell mode", colors.FgGreenBold)) em.displayConnectBanner() return } if strings.HasPrefix(in, "rcliconfig") { err := em.rcliconfig(in) if err != nil { em.log.Error(err, "Error processing rcliconfig command") } return } fmt.Println(colors.Text("Unknown command: %s", colors.FgRed, in)) } // parses connect command and replaces missing fields with data from em.workspace if // these exist func (em *Emulator) parseConnectInput(str string) (connectionData, error) { vals, err := shlex.Split(str) if err != nil { return connectionData{}, err } flags := []string{"bannerID", "storeID", "terminalID"} switch len(vals) { case 2: projectID := getProjectID(em.workspace.ProjectID) fmt.Println(colors.Text("Using bannerID, and storeID from workspace", colors.FgBlue)) for idx, item := range []string{em.workspace.BannerID, em.workspace.StoreID} { if item == "" || item == UNSET { return connectionData{}, fmt.Errorf("%s in workspace was not set", flags[idx]) } } return connectionData{ projectID: projectID, bannerID: em.workspace.BannerID, storeID: em.workspace.StoreID, terminalID: vals[1]}, nil case 3: projectID := getProjectID(em.workspace.ProjectID) fmt.Println(colors.Text("Using bannerID from workspace", colors.FgBlue)) for idx, item := range []string{em.workspace.BannerID} { if item == "" || item == UNSET { return connectionData{}, fmt.Errorf("%s in workspace was not set", flags[idx]) } } return connectionData{ projectID: projectID, bannerID: em.workspace.BannerID, storeID: vals[1], terminalID: vals[2], }, nil case 4: projectID := getProjectID(em.workspace.ProjectID) return connectionData{ projectID: projectID, bannerID: vals[1], storeID: vals[2], terminalID: vals[3], }, nil case 5: return connectionData{ projectID: vals[1], bannerID: vals[2], storeID: vals[3], terminalID: vals[4], }, nil } return connectionData{}, fmt.Errorf("wrong number of values given") } func getProjectID(projectID string) string { if projectID != "" && projectID != UNSET { fmt.Println(colors.Text("Using projectID from workspace", colors.FgBlue)) } if projectID == UNSET { // cliServiceItfc Connect expects an empty string for projectID if it // is not set rather than the const UNSET projectID = "" } return projectID } func exitChecker(in string, breakline bool) bool { if breakline { for _, item := range exitCommands { if in == item { return true } } } return false } // Function deals with all configuration options available via the rcliconfig // command at the command prompt func (em *Emulator) rcliconfig(in string) error { // TODO pass in io.writer and test? values, err := shlex.Split(in) if err != nil { fmt.Println(colors.Text("Error processing command, see log for further details", colors.FgRed)) return err } if len(values) < 2 { fmt.Println(colors.Text("Please provide a subcommand", colors.FgRed)) return ErrorRCLINoSubcmd } return em.rcliConfigSwitch(values) } func (em *Emulator) rcliConfigSwitch(values []string) error { switch values[1] { case "topicTemplate": err := checkrcliConfValues(values) if err != nil { return err } em.cls.SetTopicTemplate(values[2]) case "subscriptionTemplate": err := checkrcliConfValues(values) if err != nil { return err } em.cls.SetSubscriptionTemplate(values[2]) case "projectID": err := checkrcliConfValues(values) if err != nil { return err } em.workspace.ProjectID = values[2] em.displayWorkspace() case "bannerID": err := checkrcliConfValues(values) if err != nil { return err } em.workspace.BannerID = values[2] em.displayWorkspace() case "storeID": err := checkrcliConfValues(values) if err != nil { return err } em.workspace.StoreID = values[2] em.displayWorkspace() case "enablePerSessionSubscription": em.workspace.DisablePerSessionSubscription = false em.cls.EnablePerSessionSubscription() case "disablePerSessionSubscription": em.workspace.DisablePerSessionSubscription = true em.cls.DisablePerSessionSubscription() default: fmt.Println(colors.Text("Unknown subcommand: %s", colors.FgRed, values[1])) return ErrorRCLIUnknownSubcmd } return nil } func checkrcliConfValues(values []string) error { if len(values) != 3 { fmt.Println(colors.Text("Unexpected number of arguments for %s subcommand", colors.FgRed, values[1])) return errors.New("Unexpected number of arguments for subcommand " + values[1]) } fmt.Println(colors.Text("Setting "+values[1]+" to "+values[2], colors.FgBlue)) return nil } func (em Emulator) displayConnectBanner() { fmt.Println(colors.Text("Remote CLI: 'Ctrl-D', 'end', 'exit' or 'q' to exit. ", colors.BgBlue)) em.displayUser() em.displayWorkspace() } // prints workspace func (em Emulator) displayWorkspace() { fmt.Print(colors.Text("Workspace: ", colors.FgBlueBold)) fmt.Print(colors.Text("%s", colors.FgBlue, em.workspace.string())) } func (em Emulator) displayUser() { fmt.Print(colors.Text("User: ", colors.FgBlueBold)) fmt.Print(colors.Text("%s\n", colors.FgBlue, em.cls.UserID())) }