From 54a7d51efacafd838b909908d2702d61c4e50898 Mon Sep 17 00:00:00 2001 From: Randall Winkhart Date: Fri, 9 May 2025 16:57:57 +0000 Subject: [PATCH] Port to latest libmutton development version (use go-boilerplate) --- go.mod | 3 +- go.sum | 6 ++-- mutn.go | 7 ++-- src/cli/1globals.go | 5 ++- src/cli/add.go | 11 +++--- src/cli/edit.go | 22 ++++++------ src/cli/entryList.go | 12 +++---- src/cli/entryReader.go | 20 ++++++----- src/cli/init.go | 30 +++++++++-------- src/cli/printInfo.go | 63 +++++++++++++++++----------------- src/cli/utilitiesMisc.go | 73 ++++------------------------------------ 11 files changed, 101 insertions(+), 151 deletions(-) diff --git a/go.mod b/go.mod index aaf53c3..2399f19 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,8 @@ go 1.24.3 require ( github.com/Trojan2021/BEAN v0.0.0-20241210230804-8f294833b514 github.com/charmbracelet/glamour v0.7.0 - github.com/rwinkhart/libmutton v0.3.2-0.20250508234242-fb83d29a23e0 + github.com/rwinkhart/go-boilerplate v0.0.0-20250509154735-0846290a7620 + github.com/rwinkhart/libmutton v0.3.2-0.20250509161203-03c040b373b6 golang.org/x/term v0.32.0 ) diff --git a/go.sum b/go.sum index d96e2bd..800a0bc 100644 --- a/go.sum +++ b/go.sum @@ -59,10 +59,12 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rwinkhart/convertroman v0.2.0 h1:otUm939eXT77q/Lr31mJtMwVWEw0RjEUB71gO65h9OM= github.com/rwinkhart/convertroman v0.2.0/go.mod h1:Af6HqvX0EIM4Y3HcnNXbbRey1dLasGB7WU0+3Cowgmw= +github.com/rwinkhart/go-boilerplate v0.0.0-20250509154735-0846290a7620 h1:MjxVq+EqJgB/sxgSbPTTuzbOrU61kKa4oJvjqqdEoO4= +github.com/rwinkhart/go-boilerplate v0.0.0-20250509154735-0846290a7620/go.mod h1:cnzIF45I0FCOvE4YIB+26pLCUx2kWyY2llKYZruNaRY= github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410 h1:NhHwFM3Pgm6zRUfFKvi0p5ndjfFbVWsRwmmhyFlG4PE= github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/rwinkhart/libmutton v0.3.2-0.20250508234242-fb83d29a23e0 h1:XoJyvbNJBU3HbYHlyYBh3VKzVad10wOZ/feFkBTvxWU= -github.com/rwinkhart/libmutton v0.3.2-0.20250508234242-fb83d29a23e0/go.mod h1:IW7espY4xtdVTd0/6THtmRadR+utzH1dDXcgrP3VqlM= +github.com/rwinkhart/libmutton v0.3.2-0.20250509161203-03c040b373b6 h1:sV+cBjbQHX1++LrmGpcJ3+QOF6SYJDfPNuIywzqHK10= +github.com/rwinkhart/libmutton v0.3.2-0.20250509161203-03c040b373b6/go.mod h1:qUMh7UidlnhcAyLEygYV8z87le46MnL71r8b6MvJL4w= github.com/rwinkhart/peercred-mini v0.0.0-20250407033241-c09add2eceea h1:VE2ti/AE4Y3kgnyK+J5c5hpddE+dKHpsFch3K2R6puQ= github.com/rwinkhart/peercred-mini v0.0.0-20250407033241-c09add2eceea/go.mod h1:t+YkvAdnTKTrg4d469tw3K+GCUzX/Bja4h8yKjSIsGs= github.com/rwinkhart/rcw v0.0.0-20250508234041-b49d9f1eea42 h1:/vsBCwHP22cmqrtjsRB1OcPcb+sckktdeFlHKmhzD54= diff --git a/mutn.go b/mutn.go index a78725e..a962dff 100644 --- a/mutn.go +++ b/mutn.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/rwinkhart/MUTN/src/cli" + "github.com/rwinkhart/go-boilerplate/back" "github.com/rwinkhart/libmutton/core" "github.com/rwinkhart/libmutton/sync" ) @@ -96,7 +97,7 @@ func main() { case "note", "-n": field = 4 case "rename", "-r": - core.TargetIsFile(targetLocation, true, 0) // ensure location exists before prompting for new location + back.TargetIsFile(targetLocation, true, 0) // ensure location exists before prompting for new location cli.RenameCli(args[1]) // pass the incomplete path as the server and all clients (reading from the deletions directory) will have a different home directory default: cli.HelpEdit() @@ -161,11 +162,11 @@ func main() { case "startrcwd": core.RCWDArgument() case "sync": - cli.RunJobWrapper(true) + sync.RunJob(true, false) case "init": cli.TempInitCli() case "tweak": - core.PrintError("\"tweak\" is not yet implemented", 0, true) + back.PrintError("\"tweak\" is not yet implemented", 0, true) case "copy": cli.HelpCopy() case "edit": diff --git a/src/cli/1globals.go b/src/cli/1globals.go index 46b0ae1..973a2af 100644 --- a/src/cli/1globals.go +++ b/src/cli/1globals.go @@ -3,6 +3,7 @@ package cli import ( "os" + "github.com/rwinkhart/go-boilerplate/front" "github.com/rwinkhart/libmutton/core" "golang.org/x/term" ) @@ -19,7 +20,5 @@ const ( ) func init() { - core.GetPassphrase = func() []byte { - return inputHidden("RCW Passphrase:") - } + core.GetPassphrase = front.InputHidden } diff --git a/src/cli/add.go b/src/cli/add.go index 8fc641a..13cb81b 100644 --- a/src/cli/add.go +++ b/src/cli/add.go @@ -1,6 +1,7 @@ package cli import ( + "github.com/rwinkhart/go-boilerplate/front" "github.com/rwinkhart/libmutton/core" ) @@ -13,19 +14,19 @@ func AddEntry(targetLocation string, hideSecrets bool, entryType uint8) { var unencryptedEntry []string if entryType < 2 { - username := input("Username:") + username := front.Input("Username:") // determine whether to generate the password var password string if entryType == 0 { - password = string(inputHidden("Password:")) + password = string(front.InputHidden("Password:")) } else { password = inputPasswordGen() } - totp := string(inputHidden("TOTP secret:")) - url := input("URL:") - if inputBinary("Add a note to this entry?") { + totp := string(front.InputHidden("TOTP secret:")) + url := front.Input("URL:") + if front.InputBinary("Add a note to this entry?") { note, _ := editNote([]string{}) unencryptedEntry = append([]string{password, username, totp, url}, note...) } else { diff --git a/src/cli/edit.go b/src/cli/edit.go index 4818af2..56a00f2 100644 --- a/src/cli/edit.go +++ b/src/cli/edit.go @@ -6,6 +6,8 @@ import ( "os/exec" "reflect" + "github.com/rwinkhart/go-boilerplate/back" + "github.com/rwinkhart/go-boilerplate/front" "github.com/rwinkhart/libmutton/core" "github.com/rwinkhart/libmutton/sync" ) @@ -13,7 +15,7 @@ import ( // RenameCli renames an entry at oldLocationIncomplete to a new location (user input) on both the client and the server. func RenameCli(oldLocationIncomplete string) { // prompt user for new location and rename - newLocationIncomplete := input("New location:") + newLocationIncomplete := front.Input("New location:") sync.RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete, false) // exit is done from sync.RenameRemoteFromClient @@ -27,13 +29,13 @@ func EditEntryField(targetLocation string, hideSecrets bool, field int) { // edit the field switch field { case 0: - unencryptedEntry[field] = string(inputHidden("Password:")) + unencryptedEntry[field] = string(front.InputHidden("Password:")) case 1: - unencryptedEntry[field] = input("Username:") + unencryptedEntry[field] = front.Input("Username:") case 2: - unencryptedEntry[field] = string(inputHidden("TOTP secret:")) + unencryptedEntry[field] = string(front.InputHidden("TOTP secret:")) case 3: - unencryptedEntry[field] = input("URL:") + unencryptedEntry[field] = front.Input("URL:") case 4: // edit notes fields // store note and non-note data separately nonNoteData := unencryptedEntry[:4] @@ -42,7 +44,7 @@ func EditEntryField(targetLocation string, hideSecrets bool, field int) { // edit the note editedNote, noteEdited := editNote(noteData) if !noteEdited { // exit early if the note was not edited - core.PrintError("Entry is unchanged", 0, true) + back.PrintError("Entry is unchanged", 0, true) } unencryptedEntry = append(nonNoteData, editedNote...) } @@ -66,7 +68,7 @@ func GenUpdate(targetLocation string, hideSecrets bool) { // editNote uses the user-specified text editor to edit an existing note (or create a new one if baseNote is empty). // Returns the edited note and a boolean indicating whether the note was edited. func editNote(baseNote []string) ([]string, bool) { - tempFile := core.CreateTempFile() + tempFile := back.CreateTempFile() defer func(name string) { _ = os.Remove(name) // error ignored; if the file could be created, it can probably be removed }(tempFile.Name()) @@ -102,13 +104,13 @@ func editNote(baseNote []string) ([]string, bool) { cmd.Stderr = os.Stderr err := cmd.Run() if err != nil { - panic(core.AnsiError + "Failed to write note with " + editor + core.AnsiReset) // panic is used to ensure the tempFile is removed, as per the defer statement + panic(back.AnsiError + "Failed to write note with " + editor + back.AnsiReset) // panic is used to ensure the tempFile is removed, as per the defer statement } // open the tempFile for reading tempFile, err = os.Open(tempFile.Name()) if err != nil { - panic(core.AnsiError + "Failed to read note written with " + editor + core.AnsiReset) // panic is used to ensure the tempFile is removed, as per the defer statement + panic(back.AnsiError + "Failed to read note written with " + editor + back.AnsiReset) // panic is used to ensure the tempFile is removed, as per the defer statement } // read the edited note from the tempFile @@ -122,7 +124,7 @@ func editNote(baseNote []string) ([]string, bool) { _ = tempFile.Close() // error ignored; if the file could be opened, it can probably be closed // remove trailing empty strings from the edited note - note = core.RemoveTrailingEmptyStrings(note) + note = back.RemoveTrailingEmptyStrings(note) // clamp trailing whitespace in each note line core.ClampTrailingWhitespace(note) diff --git a/src/cli/entryList.go b/src/cli/entryList.go index 630849c..68928d5 100644 --- a/src/cli/entryList.go +++ b/src/cli/entryList.go @@ -5,7 +5,7 @@ import ( "os" "strings" - "github.com/rwinkhart/libmutton/core" + "github.com/rwinkhart/go-boilerplate/back" "github.com/rwinkhart/libmutton/sync" ) @@ -72,7 +72,7 @@ func printFileEntry(entry string, lastSlash, charCounter, indent int, colorAlter } // print fileEntryName to screen - fmt.Printf("%s%s%s ", colorCode, fileEntryName, core.AnsiReset) + fmt.Printf("%s%s%s ", colorCode, fileEntryName, back.AnsiReset) return charCounter, colorAlternator } @@ -82,7 +82,7 @@ func EntryListGen() { fileList, dirList := sync.WalkEntryDir() // print header bar w/total entry count - fmt.Print("\n"+ansiBlackOnWhite, len(fileList), " libmutton entries:"+core.AnsiReset) + fmt.Print("\n"+ansiBlackOnWhite, len(fileList), " libmutton entries:"+back.AnsiReset) // dirList iteration dirListLength := len(dirList) // save length for multiple references below @@ -128,7 +128,7 @@ func EntryListGen() { containsFiles = true skippedDirList[i] = false // the directory header is being printed, indicate that it is not being skipped indent, vanityDirectory = determineIndentation(skippedDirList, dirList, i) // calculate the final indentation multiplier - fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+core.AnsiReset+"\n", vanityDirectory) + fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+back.AnsiReset+"\n", vanityDirectory) } charCounter, colorAlternator = printFileEntry(file, lastSlash, charCounter, indent, colorAlternator) @@ -140,8 +140,8 @@ func EntryListGen() { if dirListLength > 1 { // and directories besides the root-level exist... display directory header and empty directory warning skippedDirList[i] = false // the directory header is being printed, indicate that it is not being skipped indent, vanityDirectory = determineIndentation(skippedDirList, dirList, i) // calculate the final indentation multiplier - fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+core.AnsiReset+"\n", vanityDirectory) - fmt.Print(strings.Repeat(" ", indent*2) + ansiEmptyDirectoryWarning + "-empty directory-" + core.AnsiReset) + fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+back.AnsiReset+"\n", vanityDirectory) + fmt.Print(strings.Repeat(" ", indent*2) + ansiEmptyDirectoryWarning + "-empty directory-" + back.AnsiReset) } else { // warn if the only thing that exists is the root-level directory fmt.Print("\n\nNothing's here! For help creating your first entry, run \"mutn help\".") } diff --git a/src/cli/entryReader.go b/src/cli/entryReader.go index b0e1514..a4ffba3 100644 --- a/src/cli/entryReader.go +++ b/src/cli/entryReader.go @@ -4,7 +4,9 @@ import ( "fmt" "os" + "github.com/rwinkhart/go-boilerplate/back" "github.com/rwinkhart/libmutton/core" + "github.com/rwinkhart/libmutton/sync" ) const ansiShownPassword = "\033[38;5;10m" @@ -19,33 +21,33 @@ func EntryReader(decryptedEntry []string, hideSecrets, syncEnabled bool) { // if the first field (password) is not empty, print it if decryptedEntry[0] != "" { if !hideSecrets { - fmt.Print(ansiDirectoryHeader + "Password:" + core.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[0] + core.AnsiReset + "\n\n") + fmt.Print(ansiDirectoryHeader + "Password:" + back.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[0] + back.AnsiReset + "\n\n") } else { - fmt.Print(ansiDirectoryHeader + "Password:" + core.AnsiReset + "\n" + ansiEmptyDirectoryWarning + "End command in \"show\" or \"-s\" to view" + core.AnsiReset + "\n\n") + fmt.Print(ansiDirectoryHeader + "Password:" + back.AnsiReset + "\n" + ansiEmptyDirectoryWarning + "End command in \"show\" or \"-s\" to view" + back.AnsiReset + "\n\n") } } case 1: // if the second field (username) is not empty, print it if decryptedEntry[1] != "" { - fmt.Print(ansiDirectoryHeader + "Username:" + core.AnsiReset + "\n" + decryptedEntry[1] + "\n\n") + fmt.Print(ansiDirectoryHeader + "Username:" + back.AnsiReset + "\n" + decryptedEntry[1] + "\n\n") } case 2: // if the third field (TOTP secret) is not empty, print it if decryptedEntry[2] != "" { if !hideSecrets { - fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + core.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[2] + core.AnsiReset + "\n\n") + fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + back.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[2] + back.AnsiReset + "\n\n") } else { - fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + core.AnsiReset + "\n" + ansiEmptyDirectoryWarning + "End command in \"show\" or \"-s\" to view" + core.AnsiReset + "\n\n") + fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + back.AnsiReset + "\n" + ansiEmptyDirectoryWarning + "End command in \"show\" or \"-s\" to view" + back.AnsiReset + "\n\n") } } case 3: // if the fourth field (url) is not empty, print it if decryptedEntry[3] != "" { - fmt.Print(ansiDirectoryHeader + "URL:" + core.AnsiReset + "\n" + decryptedEntry[3] + "\n\n") + fmt.Print(ansiDirectoryHeader + "URL:" + back.AnsiReset + "\n" + decryptedEntry[3] + "\n\n") } case 4: // print the notes header - fmt.Println(ansiDirectoryHeader + "Notes:" + core.AnsiReset) + fmt.Println(ansiDirectoryHeader + "Notes:" + back.AnsiReset) // combine remaining fields into a single string (to support Markdown rendering) var noteLines []string @@ -62,7 +64,7 @@ func EntryReader(decryptedEntry []string, hideSecrets, syncEnabled bool) { } if syncEnabled { - RunJobWrapper(false) + sync.RunJob(false, false) } os.Exit(0) @@ -70,7 +72,7 @@ func EntryReader(decryptedEntry []string, hideSecrets, syncEnabled bool) { // EntryReaderDecrypt is a wrapper for EntryReader that first decrypts an RCW-wrapped file before sending it to EntryReader. func EntryReaderDecrypt(targetLocation string, hideSecrets bool) { - if isFile, _ := core.TargetIsFile(targetLocation, true, 2); isFile { + if isFile, _ := back.TargetIsFile(targetLocation, true, 2); isFile { EntryReader(core.DecryptFileToSlice(targetLocation), hideSecrets, false) // never sync if decrypting straight to EntryReader, as this means the entry could not have been modified } // do not exit, as this is the job of EntryReader diff --git a/src/cli/init.go b/src/cli/init.go index d941122..c8def0a 100644 --- a/src/cli/init.go +++ b/src/cli/init.go @@ -7,6 +7,8 @@ import ( "os" "strconv" + "github.com/rwinkhart/go-boilerplate/back" + "github.com/rwinkhart/go-boilerplate/front" "github.com/rwinkhart/libmutton/core" "github.com/rwinkhart/libmutton/sync" ) @@ -14,30 +16,30 @@ import ( // TempInitCli initializes the MUTN environment based on user input. func TempInitCli() { // text editor - textEditor := cmp.Or(input("Text editor (leave blank for $EDITOR, falls back to \""+fallbackEditor+"\"):"), os.Getenv("EDITOR"), fallbackEditor) + textEditor := cmp.Or(front.Input("Text editor (leave blank for $EDITOR, falls back to \""+fallbackEditor+"\"):"), os.Getenv("EDITOR"), fallbackEditor) // SSH info - configSSH := inputBinary("Configure SSH settings (for synchronization)?") + configSSH := front.InputBinary("Configure SSH settings (for synchronization)?") if configSSH { // necessary SSH info - fmt.Println(AnsiBold + "\nNote:" + core.AnsiReset + " Only key-based authentication is supported (keys may optionally be passphrase-protected).\nThe remote server must already be in your ~" + core.PathSeparator + ".ssh" + core.PathSeparator + "known_hosts file.") - sshUser := input("Remote SSH username:") - sshPort := input("Remote SSH port:") - sshIP := input("Remote SSH IP/domain:") + fmt.Println(AnsiBold + "\nNote:" + back.AnsiReset + " Only key-based authentication is supported (keys may optionally be passphrase-protected).\nThe remote server must already be in your ~" + core.PathSeparator + ".ssh" + core.PathSeparator + "known_hosts file.") + sshUser := front.Input("Remote SSH username:") + sshPort := front.Input("Remote SSH port:") + sshIP := front.Input("Remote SSH IP/domain:") // prompt for and ensure existence of SSH identity file var sshKey string var sshKeyIsFile bool for !sshKeyIsFile { - fallbackSSHKey := core.Home + core.PathSeparator + ".ssh" + core.PathSeparator + "id_ed25519" - sshKey = cmp.Or(core.ExpandPathWithHome(input("SSH private identity file path (falls back to \""+fallbackSSHKey+"\"):")), fallbackSSHKey) - sshKeyIsFile, _ = core.TargetIsFile(sshKey, false, 0) + fallbackSSHKey := back.Home + core.PathSeparator + ".ssh" + core.PathSeparator + "id_ed25519" + sshKey = cmp.Or(back.ExpandPathWithHome(front.Input("SSH private identity file path (falls back to \""+fallbackSSHKey+"\"):")), fallbackSSHKey) + sshKeyIsFile, _ = back.TargetIsFile(sshKey, false, 0) if !sshKeyIsFile { - fmt.Println(core.AnsiError+"SSH identity file not found:", sshKey+core.AnsiReset) // do not exit after error (allow user to retry) + fmt.Println(back.AnsiError+"SSH identity file not found:", sshKey+back.AnsiReset) // do not exit after error (allow user to retry) } } - sshKeyProtected := inputBinary("Is the identity file password-protected?") + sshKeyProtected := front.InputBinary("Is the identity file password-protected?") // initialize libmutton directories oldDeviceID := core.DirInit(false) @@ -60,10 +62,10 @@ func TempInitCli() { // RCW sanity check file var passphraseInput1 []byte for { - passphraseInput1 = inputHidden("New master (RCW) passphrase:") - passphraseInput2 := inputHidden("Confirm RCW passphrase:") + passphraseInput1 = front.InputHidden("New master (RCW) passphrase:") + passphraseInput2 := front.InputHidden("Confirm RCW passphrase:") if !bytes.Equal(passphraseInput1, passphraseInput2) { - fmt.Println(core.AnsiError + "Passphrases do not match" + core.AnsiReset) + fmt.Println(back.AnsiError + "Passphrases do not match" + back.AnsiReset) continue } core.RCWSanityCheckGen(passphraseInput1) diff --git a/src/cli/printInfo.go b/src/cli/printInfo.go index 64eee13..c0928fe 100644 --- a/src/cli/printInfo.go +++ b/src/cli/printInfo.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "github.com/rwinkhart/go-boilerplate/back" "github.com/rwinkhart/libmutton/core" ) @@ -14,13 +15,13 @@ const ( ) func HelpMain() { - fmt.Print(AnsiBold + "\nMUTN | Copyright (c) 2024-2025 Randall Winkhart\n" + core.AnsiReset + ` + fmt.Print(AnsiBold + "\nMUTN | Copyright (c) 2024-2025 Randall Winkhart\n" + back.AnsiReset + ` This software exists under the MIT license; you may redistribute it under certain conditions. This program comes with absolutely no warranty; type "mutn version" for details. -` + AnsiBold + "Usage:" + core.AnsiReset + ` mutn [/ [argument] [option]] | [argument] +` + AnsiBold + "Usage:" + back.AnsiReset + ` mutn [/ [argument] [option]] | [argument] -` + AnsiBold + "Arguments:" + core.AnsiReset + ` +` + AnsiBold + "Arguments:" + back.AnsiReset + ` help Bring up this menu version Display version and license information init Set up MUTN (generates libmutton.ini) @@ -31,7 +32,7 @@ This program comes with absolutely no warranty; type "mutn version" for details. shear Delete an existing entry sync Manually sync the entry directory -` + AnsiBold + "Options:" + core.AnsiReset + ` +` + AnsiBold + "Options:" + back.AnsiReset + ` copy: password|-pw| Copy the password in an entry to your clipboard username|-u Copy the username in an entry to your clipboard @@ -52,17 +53,17 @@ This program comes with absolutely no warranty; type "mutn version" for details. note|-n Add a note entry folder|-f Add a new folder for entries -` + AnsiBold + "Tip 1:" + core.AnsiReset + ` You can quickly read an entry with "mutn /" -` + AnsiBold + "Tip 2:" + core.AnsiReset + ` Type "mutn" (no arguments/options) to view a list of saved entries -` + AnsiBold + "Tip 3:" + core.AnsiReset + ` Provide "add", "edit", "copy", or "gen" as the only argument to receive more specific help -` + AnsiBold + "Tip 4:" + core.AnsiReset + " Using \"add\", \"edit\", or \"copy\" without specifying an option (field) will default to \"password\"\n\n") +` + AnsiBold + "Tip 1:" + back.AnsiReset + ` You can quickly read an entry with "mutn /" +` + AnsiBold + "Tip 2:" + back.AnsiReset + ` Type "mutn" (no arguments/options) to view a list of saved entries +` + AnsiBold + "Tip 3:" + back.AnsiReset + ` Provide "add", "edit", "copy", or "gen" as the only argument to receive more specific help +` + AnsiBold + "Tip 4:" + back.AnsiReset + " Using \"add\", \"edit\", or \"copy\" without specifying an option (field) will default to \"password\"\n\n") os.Exit(0) } func HelpAdd() { - fmt.Print(AnsiBold + "\nUsage:" + core.AnsiReset + ` mutn / add