github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/config/ui.go (about) 1 // Textual user interface parts of the config system 2 3 package config 4 5 import ( 6 "bufio" 7 "context" 8 "errors" 9 "fmt" 10 "log" 11 "os" 12 "sort" 13 "strconv" 14 "strings" 15 "unicode/utf8" 16 17 "github.com/rclone/rclone/fs" 18 "github.com/rclone/rclone/fs/config/configmap" 19 "github.com/rclone/rclone/fs/config/configstruct" 20 "github.com/rclone/rclone/fs/config/obscure" 21 "github.com/rclone/rclone/fs/driveletter" 22 "github.com/rclone/rclone/fs/fspath" 23 "github.com/rclone/rclone/lib/terminal" 24 "golang.org/x/text/unicode/norm" 25 ) 26 27 // ReadLine reads some input 28 var ReadLine = func() string { 29 buf := bufio.NewReader(os.Stdin) 30 line, err := buf.ReadString('\n') 31 if err != nil { 32 log.Fatalf("Failed to read line: %v", err) 33 } 34 return strings.TrimSpace(line) 35 } 36 37 // ReadNonEmptyLine prints prompt and calls Readline until non empty 38 func ReadNonEmptyLine(prompt string) string { 39 result := "" 40 for result == "" { 41 fmt.Print(prompt) 42 result = strings.TrimSpace(ReadLine()) 43 } 44 return result 45 } 46 47 // CommandDefault - choose one. If return is pressed then it will 48 // chose the defaultIndex if it is >= 0 49 // 50 // Must not call fs.Log anything from here to avoid deadlock in 51 // --interactive --progress 52 func CommandDefault(commands []string, defaultIndex int) byte { 53 opts := []string{} 54 for i, text := range commands { 55 def := "" 56 if i == defaultIndex { 57 def = " (default)" 58 } 59 fmt.Printf("%c) %s%s\n", text[0], text[1:], def) 60 opts = append(opts, text[:1]) 61 } 62 optString := strings.Join(opts, "") 63 optHelp := strings.Join(opts, "/") 64 for { 65 fmt.Printf("%s> ", optHelp) 66 result := strings.ToLower(ReadLine()) 67 if len(result) == 0 { 68 if defaultIndex >= 0 { 69 return optString[defaultIndex] 70 } 71 fmt.Printf("This value is required and it has no default.\n") 72 } else if len(result) == 1 { 73 i := strings.Index(optString, string(result[0])) 74 if i >= 0 { 75 return result[0] 76 } 77 fmt.Printf("This value must be one of the following characters: %s.\n", strings.Join(opts, ", ")) 78 } else { 79 fmt.Printf("This value must be a single character, one of the following: %s.\n", strings.Join(opts, ", ")) 80 } 81 } 82 } 83 84 // Command - choose one 85 func Command(commands []string) byte { 86 return CommandDefault(commands, -1) 87 } 88 89 // Confirm asks the user for Yes or No and returns true or false 90 // 91 // If the user presses enter then the Default will be used 92 func Confirm(Default bool) bool { 93 defaultIndex := 0 94 if !Default { 95 defaultIndex = 1 96 } 97 return CommandDefault([]string{"yYes", "nNo"}, defaultIndex) == 'y' 98 } 99 100 // Choose one of the choices, or default, or type a new string if newOk is set 101 func Choose(what string, kind string, choices, help []string, defaultValue string, required bool, newOk bool) string { 102 valueDescription := "an existing" 103 if newOk { 104 valueDescription = "your own" 105 } 106 fmt.Printf("Choose a number from below, or type in %s %s.\n", valueDescription, kind) 107 // Empty input is allowed if not required is set, or if 108 // required is set but there is a default value to use. 109 if defaultValue != "" { 110 fmt.Printf("Press Enter for the default (%s).\n", defaultValue) 111 } else if !required { 112 fmt.Printf("Press Enter to leave empty.\n") 113 } 114 attributes := []string{terminal.HiRedFg, terminal.HiGreenFg} 115 for i, text := range choices { 116 var lines []string 117 if help != nil && help[i] != "" { 118 parts := strings.Split(help[i], "\n") 119 lines = append(lines, parts...) 120 lines = append(lines, fmt.Sprintf("(%s)", text)) 121 } 122 pos := i + 1 123 terminal.WriteString(attributes[i%len(attributes)]) 124 if len(lines) == 0 { 125 fmt.Printf("%2d > %s\n", pos, text) 126 } else { 127 mid := (len(lines) - 1) / 2 128 for i, line := range lines { 129 var sep rune 130 switch i { 131 case 0: 132 sep = '/' 133 case len(lines) - 1: 134 sep = '\\' 135 default: 136 sep = '|' 137 } 138 number := " " 139 if i == mid { 140 number = fmt.Sprintf("%2d", pos) 141 } 142 fmt.Printf("%s %c %s\n", number, sep, line) 143 } 144 } 145 terminal.WriteString(terminal.Reset) 146 } 147 for { 148 fmt.Printf("%s> ", what) 149 result := ReadLine() 150 i, err := strconv.Atoi(result) 151 if err != nil { 152 for _, v := range choices { 153 if result == v { 154 return result 155 } 156 } 157 if result == "" { 158 // If empty string is in the predefined list of choices it has already been returned above. 159 // If parameter required is not set, then empty string is always a valid value. 160 if !required { 161 return result 162 } 163 // If parameter required is set, but there is a default, then empty input means default. 164 if defaultValue != "" { 165 return defaultValue 166 } 167 // If parameter required is set, and there is no default, then an input value is required. 168 fmt.Printf("This value is required and it has no default.\n") 169 } else if newOk { 170 // If legal input is not restricted to defined choices, then any nonzero input string is accepted. 171 return result 172 } else { 173 // A nonzero input string was specified but it did not match any of the strictly defined choices. 174 fmt.Printf("This value must match %s value.\n", valueDescription) 175 } 176 } else { 177 if i >= 1 && i <= len(choices) { 178 return choices[i-1] 179 } 180 fmt.Printf("No choices with this number.\n") 181 } 182 } 183 } 184 185 // Enter prompts for an input value of a specified type 186 func Enter(what string, kind string, defaultValue string, required bool) string { 187 // Empty input is allowed if not required is set, or if 188 // required is set but there is a default value to use. 189 fmt.Printf("Enter a %s.", kind) 190 if defaultValue != "" { 191 fmt.Printf(" Press Enter for the default (%s).\n", defaultValue) 192 } else if !required { 193 fmt.Println(" Press Enter to leave empty.") 194 } else { 195 fmt.Println() 196 } 197 for { 198 fmt.Printf("%s> ", what) 199 result := ReadLine() 200 if !required || result != "" { 201 return result 202 } 203 if defaultValue != "" { 204 return defaultValue 205 } 206 fmt.Printf("This value is required and it has no default.\n") 207 } 208 } 209 210 // ChoosePassword asks the user for a password 211 func ChoosePassword(defaultValue string, required bool) string { 212 fmt.Printf("Choose an alternative below.") 213 actions := []string{"yYes, type in my own password", "gGenerate random password"} 214 defaultAction := -1 215 if defaultValue != "" { 216 defaultAction = len(actions) 217 actions = append(actions, "nNo, keep existing") 218 fmt.Printf(" Press Enter for the default (%s).", string(actions[defaultAction][0])) 219 } else if !required { 220 defaultAction = len(actions) 221 actions = append(actions, "nNo, leave this optional password blank") 222 fmt.Printf(" Press Enter for the default (%s).", string(actions[defaultAction][0])) 223 } 224 fmt.Println() 225 var password string 226 var err error 227 switch i := CommandDefault(actions, defaultAction); i { 228 case 'y': 229 password = ChangePassword("the") 230 case 'g': 231 for { 232 fmt.Printf("Password strength in bits.\n64 is just about memorable\n128 is secure\n1024 is the maximum\n") 233 bits := ChooseNumber("Bits", 64, 1024) 234 password, err = Password(bits) 235 if err != nil { 236 log.Fatalf("Failed to make password: %v", err) 237 } 238 fmt.Printf("Your password is: %s\n", password) 239 fmt.Printf("Use this password? Please note that an obscured version of this \npassword (and not the " + 240 "password itself) will be stored under your \nconfiguration file, so keep this generated password " + 241 "in a safe place.\n") 242 if Confirm(true) { 243 break 244 } 245 } 246 case 'n': 247 return defaultValue 248 default: 249 fs.Errorf(nil, "Bad choice %c", i) 250 } 251 return obscure.MustObscure(password) 252 } 253 254 // ChooseNumber asks the user to enter a number between min and max 255 // inclusive prompting them with what. 256 func ChooseNumber(what string, min, max int) int { 257 for { 258 fmt.Printf("%s> ", what) 259 result := ReadLine() 260 i, err := strconv.Atoi(result) 261 if err != nil { 262 fmt.Printf("Bad number: %v\n", err) 263 continue 264 } 265 if i < min || i > max { 266 fmt.Printf("Out of range - %d to %d inclusive\n", min, max) 267 continue 268 } 269 return i 270 } 271 } 272 273 // ShowRemotes shows an overview of the config file 274 func ShowRemotes() { 275 remotes := LoadedData().GetSectionList() 276 if len(remotes) == 0 { 277 return 278 } 279 sort.Strings(remotes) 280 fmt.Printf("%-20s %s\n", "Name", "Type") 281 fmt.Printf("%-20s %s\n", "====", "====") 282 for _, remote := range remotes { 283 fmt.Printf("%-20s %s\n", remote, FileGet(remote, "type")) 284 } 285 } 286 287 // ChooseRemote chooses a remote name 288 func ChooseRemote() string { 289 remotes := LoadedData().GetSectionList() 290 sort.Strings(remotes) 291 fmt.Println("Select remote.") 292 return Choose("remote", "value", remotes, nil, "", true, false) 293 } 294 295 // mustFindByName finds the RegInfo for the remote name passed in or 296 // exits with a fatal error. 297 func mustFindByName(name string) *fs.RegInfo { 298 fsType := FileGet(name, "type") 299 if fsType == "" { 300 log.Fatalf("Couldn't find type of fs for %q", name) 301 } 302 return fs.MustFind(fsType) 303 } 304 305 // findByName finds the RegInfo for the remote name passed in or 306 // returns an error 307 func findByName(name string) (*fs.RegInfo, error) { 308 fsType := FileGet(name, "type") 309 if fsType == "" { 310 return nil, fmt.Errorf("couldn't find type of fs for %q", name) 311 } 312 return fs.Find(fsType) 313 } 314 315 // printRemoteOptions prints the options of the remote 316 func printRemoteOptions(name string, prefix string, sep string, redacted bool) { 317 fsInfo, err := findByName(name) 318 if err != nil { 319 fmt.Printf("# %v\n", err) 320 fsInfo = nil 321 } 322 for _, key := range LoadedData().GetKeyList(name) { 323 isPassword := false 324 isSensitive := false 325 if fsInfo != nil { 326 for _, option := range fsInfo.Options { 327 if option.Name == key { 328 if option.IsPassword { 329 isPassword = true 330 } else if option.Sensitive { 331 isSensitive = true 332 } 333 } 334 } 335 } 336 value := FileGet(name, key) 337 if redacted && (isSensitive || isPassword) && value != "" { 338 fmt.Printf("%s%s%sXXX\n", prefix, key, sep) 339 } else if isPassword && value != "" { 340 fmt.Printf("%s%s%s*** ENCRYPTED ***\n", prefix, key, sep) 341 } else { 342 fmt.Printf("%s%s%s%s\n", prefix, key, sep, value) 343 } 344 } 345 } 346 347 // listRemoteOptions lists the options of the remote 348 func listRemoteOptions(name string) { 349 printRemoteOptions(name, "- ", ": ", false) 350 } 351 352 // ShowRemote shows the contents of the remote in config file format 353 func ShowRemote(name string) { 354 fmt.Printf("[%s]\n", name) 355 printRemoteOptions(name, "", " = ", false) 356 } 357 358 // ShowRedactedRemote shows the contents of the remote in config file format 359 func ShowRedactedRemote(name string) { 360 fmt.Printf("[%s]\n", name) 361 printRemoteOptions(name, "", " = ", true) 362 } 363 364 // OkRemote prints the contents of the remote and ask if it is OK 365 func OkRemote(name string) bool { 366 fmt.Println("Configuration complete.") 367 fmt.Println("Options:") 368 listRemoteOptions(name) 369 fmt.Printf("Keep this %q remote?\n", name) 370 switch i := CommandDefault([]string{"yYes this is OK", "eEdit this remote", "dDelete this remote"}, 0); i { 371 case 'y': 372 return true 373 case 'e': 374 return false 375 case 'd': 376 LoadedData().DeleteSection(name) 377 return true 378 default: 379 fs.Errorf(nil, "Bad choice %c", i) 380 } 381 return false 382 } 383 384 // newSection prints an empty line to separate sections 385 func newSection() { 386 fmt.Println() 387 } 388 389 // backendConfig configures the backend starting from the state passed in 390 // 391 // The is the user interface loop that drives the post configuration backend config. 392 func backendConfig(ctx context.Context, name string, m configmap.Mapper, ri *fs.RegInfo, choices configmap.Getter, startState string) error { 393 in := fs.ConfigIn{ 394 State: startState, 395 } 396 for { 397 out, err := fs.BackendConfig(ctx, name, m, ri, choices, in) 398 if err != nil { 399 return err 400 } 401 if out == nil { 402 break 403 } 404 if out.Error != "" { 405 fmt.Println(out.Error) 406 } 407 in.State = out.State 408 in.Result = out.Result 409 if out.Option != nil { 410 fs.Debugf(name, "config: reading config parameter %q", out.Option.Name) 411 if out.Option.Default == nil { 412 out.Option.Default = "" 413 } 414 if Default, isBool := out.Option.Default.(bool); isBool && 415 len(out.Option.Examples) == 2 && 416 out.Option.Examples[0].Help == "Yes" && 417 out.Option.Examples[0].Value == "true" && 418 out.Option.Examples[1].Help == "No" && 419 out.Option.Examples[1].Value == "false" && 420 out.Option.Exclusive { 421 // Use Confirm for Yes/No questions as it has a nicer interface= 422 fmt.Println(out.Option.Help) 423 in.Result = fmt.Sprint(Confirm(Default)) 424 } else { 425 value := ChooseOption(out.Option, name) 426 if value != "" { 427 err := out.Option.Set(value) 428 if err != nil { 429 return fmt.Errorf("failed to set option: %w", err) 430 } 431 } 432 in.Result = out.Option.String() 433 } 434 } 435 if out.State == "" { 436 break 437 } 438 newSection() 439 } 440 return nil 441 } 442 443 // PostConfig configures the backend after the main config has been done 444 // 445 // The is the user interface loop that drives the post configuration backend config. 446 func PostConfig(ctx context.Context, name string, m configmap.Mapper, ri *fs.RegInfo) error { 447 if ri.Config == nil { 448 return errors.New("backend doesn't support reconnect or authorize") 449 } 450 return backendConfig(ctx, name, m, ri, configmap.Simple{}, "") 451 } 452 453 // RemoteConfig runs the config helper for the remote if needed 454 func RemoteConfig(ctx context.Context, name string) error { 455 fmt.Printf("Remote config\n") 456 ri := mustFindByName(name) 457 m := fs.ConfigMap(ri, name, nil) 458 if ri.Config == nil { 459 return nil 460 } 461 return PostConfig(ctx, name, m, ri) 462 } 463 464 // ChooseOption asks the user to choose an option 465 func ChooseOption(o *fs.Option, name string) string { 466 fmt.Printf("Option %s.\n", o.Name) 467 if o.Help != "" { 468 // Show help string without empty lines. 469 help := strings.ReplaceAll(strings.TrimSpace(o.Help), "\n\n", "\n") 470 fmt.Println(help) 471 } 472 473 var defaultValue string 474 if o.Default == nil { 475 defaultValue = "" 476 } else { 477 defaultValue = fmt.Sprint(o.Default) 478 } 479 480 if o.IsPassword { 481 return ChoosePassword(defaultValue, o.Required) 482 } 483 484 what := "value" 485 if o.Default != "" { 486 switch o.Default.(type) { 487 case bool: 488 what = "boolean value (true or false)" 489 case fs.SizeSuffix: 490 what = "size with suffix K,M,G,T" 491 case fs.Duration: 492 what = "duration s,m,h,d,w,M,y" 493 case int, int8, int16, int32, int64: 494 what = "signed integer" 495 case uint, byte, uint16, uint32, uint64: 496 what = "unsigned integer" 497 default: 498 what = fmt.Sprintf("value of type %s", o.Type()) 499 } 500 } 501 var in string 502 for { 503 if len(o.Examples) > 0 { 504 var values []string 505 var help []string 506 for _, example := range o.Examples { 507 values = append(values, example.Value) 508 help = append(help, example.Help) 509 } 510 in = Choose(o.Name, what, values, help, defaultValue, o.Required, !o.Exclusive) 511 } else { 512 in = Enter(o.Name, what, defaultValue, o.Required) 513 } 514 if in != "" { 515 newIn, err := configstruct.StringToInterface(o.Default, in) 516 if err != nil { 517 fmt.Printf("Failed to parse %q: %v\n", in, err) 518 continue 519 } 520 in = fmt.Sprint(newIn) // canonicalise 521 } 522 return in 523 } 524 } 525 526 // NewRemoteName asks the user for a name for a new remote 527 func NewRemoteName() (name string) { 528 for { 529 fmt.Println("Enter name for new remote.") 530 fmt.Printf("name> ") 531 name = ReadLine() 532 if LoadedData().HasSection(name) { 533 fmt.Printf("Remote %q already exists.\n", name) 534 continue 535 } 536 err := fspath.CheckConfigName(name) 537 switch { 538 case name == "": 539 fmt.Printf("Can't use empty name.\n") 540 case driveletter.IsDriveLetter(name): 541 fmt.Printf("Can't use %q as it can be confused with a drive letter.\n", name) 542 case err != nil: 543 fmt.Printf("Can't use %q as %v.\n", name, err) 544 default: 545 return name 546 } 547 } 548 } 549 550 // NewRemote make a new remote from its name 551 func NewRemote(ctx context.Context, name string) error { 552 var ( 553 newType string 554 ri *fs.RegInfo 555 err error 556 ) 557 558 // Set the type first 559 for { 560 newType = ChooseOption(fsOption(), name) 561 ri, err = fs.Find(newType) 562 if err != nil { 563 fmt.Printf("Bad remote %q: %v\n", newType, err) 564 continue 565 } 566 break 567 } 568 LoadedData().SetValue(name, "type", newType) 569 newSection() 570 _, err = CreateRemote(ctx, name, newType, nil, UpdateRemoteOpt{ 571 All: true, 572 }) 573 if err != nil { 574 return err 575 } 576 if OkRemote(name) { 577 SaveConfig() 578 return nil 579 } 580 newSection() 581 return EditRemote(ctx, ri, name) 582 } 583 584 // EditRemote gets the user to edit a remote 585 func EditRemote(ctx context.Context, ri *fs.RegInfo, name string) error { 586 fmt.Printf("Editing existing %q remote with options:\n", name) 587 listRemoteOptions(name) 588 newSection() 589 for { 590 _, err := UpdateRemote(ctx, name, nil, UpdateRemoteOpt{ 591 All: true, 592 }) 593 if err != nil { 594 return err 595 } 596 if OkRemote(name) { 597 break 598 } 599 } 600 SaveConfig() 601 return nil 602 } 603 604 // DeleteRemote gets the user to delete a remote 605 func DeleteRemote(name string) { 606 LoadedData().DeleteSection(name) 607 SaveConfig() 608 } 609 610 // copyRemote asks the user for a new remote name and copies name into 611 // it. Returns the new name. 612 func copyRemote(name string) string { 613 newName := NewRemoteName() 614 // Copy the keys 615 for _, key := range LoadedData().GetKeyList(name) { 616 value := getWithDefault(name, key, "") 617 LoadedData().SetValue(newName, key, value) 618 } 619 return newName 620 } 621 622 // RenameRemote renames a config section 623 func RenameRemote(name string) { 624 fmt.Printf("Enter new name for %q remote.\n", name) 625 newName := copyRemote(name) 626 if name != newName { 627 LoadedData().DeleteSection(name) 628 SaveConfig() 629 } 630 } 631 632 // CopyRemote copies a config section 633 func CopyRemote(name string) { 634 fmt.Printf("Enter name for copy of %q remote.\n", name) 635 copyRemote(name) 636 SaveConfig() 637 } 638 639 // ShowConfigLocation prints the location of the config file in use 640 func ShowConfigLocation() { 641 if configPath := GetConfigPath(); configPath == "" { 642 fmt.Println("Configuration is in memory only") 643 } else { 644 if _, err := os.Stat(configPath); os.IsNotExist(err) { 645 fmt.Println("Configuration file doesn't exist, but rclone will use this path:") 646 } else { 647 fmt.Println("Configuration file is stored at:") 648 } 649 fmt.Printf("%s\n", configPath) 650 } 651 } 652 653 // ShowConfig prints the (unencrypted) config options 654 func ShowConfig() { 655 str, err := LoadedData().Serialize() 656 if err != nil { 657 log.Fatalf("Failed to serialize config: %v", err) 658 } 659 if str == "" { 660 str = "; empty config\n" 661 } 662 fmt.Printf("%s", str) 663 } 664 665 // ShowRedactedConfig prints the redacted (unencrypted) config options 666 func ShowRedactedConfig() { 667 remotes := LoadedData().GetSectionList() 668 if len(remotes) == 0 { 669 fmt.Println("; empty config") 670 return 671 } 672 sort.Strings(remotes) 673 for i, remote := range remotes { 674 if i != 0 { 675 fmt.Println() 676 } 677 ShowRedactedRemote(remote) 678 } 679 } 680 681 // EditConfig edits the config file interactively 682 func EditConfig(ctx context.Context) (err error) { 683 for { 684 haveRemotes := len(LoadedData().GetSectionList()) != 0 685 what := []string{"eEdit existing remote", "nNew remote", "dDelete remote", "rRename remote", "cCopy remote", "sSet configuration password", "qQuit config"} 686 if haveRemotes { 687 fmt.Printf("Current remotes:\n\n") 688 ShowRemotes() 689 fmt.Printf("\n") 690 } else { 691 fmt.Printf("No remotes found, make a new one?\n") 692 // take 2nd item and last 2 items of menu list 693 what = append(what[1:2], what[len(what)-2:]...) 694 } 695 switch i := Command(what); i { 696 case 'e': 697 newSection() 698 name := ChooseRemote() 699 newSection() 700 fs := mustFindByName(name) 701 err = EditRemote(ctx, fs, name) 702 if err != nil { 703 return err 704 } 705 case 'n': 706 newSection() 707 name := NewRemoteName() 708 newSection() 709 err = NewRemote(ctx, name) 710 if err != nil { 711 return err 712 } 713 case 'd': 714 newSection() 715 name := ChooseRemote() 716 newSection() 717 DeleteRemote(name) 718 case 'r': 719 newSection() 720 name := ChooseRemote() 721 newSection() 722 RenameRemote(name) 723 case 'c': 724 newSection() 725 name := ChooseRemote() 726 newSection() 727 CopyRemote(name) 728 case 's': 729 newSection() 730 SetPassword() 731 case 'q': 732 return nil 733 } 734 newSection() 735 } 736 } 737 738 // Suppress the confirm prompts by altering the context config 739 func suppressConfirm(ctx context.Context) context.Context { 740 newCtx, ci := fs.AddConfig(ctx) 741 ci.AutoConfirm = true 742 return newCtx 743 } 744 745 // checkPassword normalises and validates the password 746 func checkPassword(password string) (string, error) { 747 if !utf8.ValidString(password) { 748 return "", errors.New("password contains invalid utf8 characters") 749 } 750 // Check for leading/trailing whitespace 751 trimmedPassword := strings.TrimSpace(password) 752 // Warn user if password has leading+trailing whitespace 753 if len(password) != len(trimmedPassword) { 754 _, _ = fmt.Fprintln(os.Stderr, "Your password contains leading/trailing whitespace - in previous versions of rclone this was stripped") 755 } 756 // Normalize to reduce weird variations. 757 password = norm.NFKC.String(password) 758 if len(password) == 0 || len(trimmedPassword) == 0 { 759 return "", errors.New("no characters in password") 760 } 761 return password, nil 762 } 763 764 // GetPassword asks the user for a password with the prompt given. 765 func GetPassword(prompt string) string { 766 _, _ = fmt.Fprintln(PasswordPromptOutput, prompt) 767 for { 768 _, _ = fmt.Fprint(PasswordPromptOutput, "password:") 769 password := ReadPassword() 770 password, err := checkPassword(password) 771 if err == nil { 772 return password 773 } 774 _, _ = fmt.Fprintf(os.Stderr, "Bad password: %v\n", err) 775 } 776 } 777 778 // ChangePassword will query the user twice for the named password. If 779 // the same password is entered it is returned. 780 func ChangePassword(name string) string { 781 for { 782 a := GetPassword(fmt.Sprintf("Enter %s password:", name)) 783 b := GetPassword(fmt.Sprintf("Confirm %s password:", name)) 784 if a == b { 785 return a 786 } 787 fmt.Println("Passwords do not match!") 788 } 789 } 790 791 // SetPassword will allow the user to modify the current 792 // configuration encryption settings. 793 func SetPassword() { 794 for { 795 if len(configKey) > 0 { 796 fmt.Println("Your configuration is encrypted.") 797 what := []string{"cChange Password", "uUnencrypt configuration", "qQuit to main menu"} 798 switch i := Command(what); i { 799 case 'c': 800 changeConfigPassword() 801 SaveConfig() 802 fmt.Println("Password changed") 803 continue 804 case 'u': 805 configKey = nil 806 SaveConfig() 807 continue 808 case 'q': 809 return 810 } 811 812 } else { 813 fmt.Println("Your configuration is not encrypted.") 814 fmt.Println("If you add a password, you will protect your login information to cloud services.") 815 what := []string{"aAdd Password", "qQuit to main menu"} 816 switch i := Command(what); i { 817 case 'a': 818 changeConfigPassword() 819 SaveConfig() 820 fmt.Println("Password set") 821 continue 822 case 'q': 823 return 824 } 825 } 826 } 827 }