From f33eb3d4802b96b114acbdf0cd016f29ded5b89c Mon Sep 17 00:00:00 2001 From: Randall Winkhart Date: Mon, 15 Apr 2024 18:16:05 -0400 Subject: [PATCH] Refactor for better modularity (share more code among edit functions, rename "offline" package to "backend" --- commit.sh | 2 +- src/cli/1globals.go | 4 +-- src/cli/add.go | 10 +++---- src/cli/edit.go | 62 +++++++++++++++------------------------- src/cli/entryList.go | 22 +++++++------- src/cli/entryListUNIX.go | 4 +-- src/cli/entryListWIN.go | 4 +-- src/cli/entryReader.go | 22 +++++++------- src/cli/init.go | 12 ++++---- src/cli/printInfo.go | 60 +++++++++++++++++++------------------- src/cli/utilitiesMisc.go | 14 ++++----- 11 files changed, 100 insertions(+), 116 deletions(-) diff --git a/commit.sh b/commit.sh index 729c383..390d41e 100755 --- a/commit.sh +++ b/commit.sh @@ -1,6 +1,6 @@ #!/bin/sh gofmt -l -w -s ./src/cli/*.go -gofmt -l -w -s ./src/offline/*.go +gofmt -l -w -s ./src/backend/*.go git add -f extra src .gitignore commit.sh go.mod go.sum LICENSE main.go README.md git commit -m "$1" git push diff --git a/src/cli/1globals.go b/src/cli/1globals.go index 55b127a..04918fa 100644 --- a/src/cli/1globals.go +++ b/src/cli/1globals.go @@ -1,14 +1,14 @@ package cli import ( - "github.com/rwinkhart/MUTN/src/offline" + "github.com/rwinkhart/MUTN/src/backend" termy "golang.org/x/crypto/ssh/terminal" "os" ) // global variables used across multiple files var ( - rootLength = len(offline.EntryRoot) + rootLength = len(backend.EntryRoot) width, _, _ = termy.GetSize(int(os.Stdout.Fd())) ) diff --git a/src/cli/add.go b/src/cli/add.go index e0a8cd5..5aaa525 100644 --- a/src/cli/add.go +++ b/src/cli/add.go @@ -2,16 +2,16 @@ package cli import ( "fmt" - "github.com/rwinkhart/MUTN/src/offline" + "github.com/rwinkhart/MUTN/src/backend" "os" ) // AddEntry creates a new entry at targetLocation by taking user input via CLI prompts // entryType: 0 = standard (password), 1 = auto-generated password, 2 = note func AddEntry(targetLocation string, hidePassword bool, entryType uint8) { - _, isAccessible := offline.TargetIsFile(targetLocation, false, 0) + _, isAccessible := backend.TargetIsFile(targetLocation, false, 0) if isAccessible { - fmt.Println(offline.AnsiError + "Target location already exists" + offline.AnsiReset) + fmt.Println(backend.AnsiError + "Target location already exists" + backend.AnsiReset) os.Exit(1) } @@ -25,7 +25,7 @@ func AddEntry(targetLocation string, hidePassword bool, entryType uint8) { if entryType == 0 { password = inputHidden("Password:") } else { - password = offline.StringGen(inputInt("Password length:", -1), inputBinary("Generate a complex (special characters) password?"), 0.2) + password = backend.StringGen(inputInt("Password length:", -1), inputBinary("Generate a complex (special characters) password?"), 0.2) } url := input("URL:") @@ -41,5 +41,5 @@ func AddEntry(targetLocation string, hidePassword bool, entryType uint8) { } // write and preview the new entry - writeEntryShortcut(targetLocation, unencryptedEntry, hidePassword) + writeEntryCLI(targetLocation, unencryptedEntry, hidePassword) } diff --git a/src/cli/edit.go b/src/cli/edit.go index 3c2f83e..36262fa 100644 --- a/src/cli/edit.go +++ b/src/cli/edit.go @@ -3,7 +3,7 @@ package cli import ( "bufio" "fmt" - "github.com/rwinkhart/MUTN/src/offline" + "github.com/rwinkhart/MUTN/src/backend" "os" "os/exec" "reflect" @@ -13,25 +13,19 @@ import ( // RenameCli renames an entry at oldLocation to a new location (user input) func RenameCli(oldLocation string) { // ensure targetLocation exists - offline.TargetIsFile(oldLocation, true, 0) + backend.TargetIsFile(oldLocation, true, 0) // prompt user for new location and rename - newLocation := offline.EntryRoot + offline.PathSeparator + input("New location:") - offline.Rename(oldLocation, newLocation) + newLocation := backend.EntryRoot + backend.PathSeparator + input("New location:") + backend.Rename(oldLocation, newLocation) - // exit is done from offline.Rename + // exit is done from backend.Rename } -// EditEntry edits a field of an entry at targetLocation (user input), does not allow for editing notes -func EditEntry(targetLocation string, hidePassword bool, field int) { - // ensure targetLocation exists - offline.TargetIsFile(targetLocation, true, 2) - - // read old entry data - unencryptedEntry := offline.DecryptGPG(targetLocation) - - // ensure slice is long enough for field - unencryptedEntry = offline.EnsureSliceLength(unencryptedEntry, field) +// EditEntryField edits a field of an entry at targetLocation (user input), does not allow for editing notes +func EditEntryField(targetLocation string, hidePassword bool, field int) { + // fetch old entry data (with all required lines present) + unencryptedEntry := backend.GetOldEntryData(targetLocation, field) // edit the field switch field { @@ -44,20 +38,13 @@ func EditEntry(targetLocation string, hidePassword bool, field int) { } // write and preview the modified entry - writeEntryShortcut(targetLocation, unencryptedEntry, hidePassword) + writeEntryCLI(targetLocation, unencryptedEntry, hidePassword) } // EditEntryNote edits the note of an entry at targetLocation (user input) func EditEntryNote(targetLocation string, hidePassword bool) { - // ensure targetLocation exists - offline.TargetIsFile(targetLocation, true, 2) - - // read old entry data - unencryptedEntry := offline.DecryptGPG(targetLocation) - - // ensure slice is long enough for note - // avoids errors when storing non-note data - unencryptedEntry = offline.EnsureSliceLength(unencryptedEntry, 2) // 2 is used because it is the index of URL, the last non-note field + // fetch old entry data (with all required lines present) + unencryptedEntry := backend.GetOldEntryData(targetLocation, 2) // store non-note data separately nonNoteData := unencryptedEntry[:3] @@ -68,36 +55,33 @@ func EditEntryNote(targetLocation string, hidePassword bool) { // edit the note editedNote, noteEdited := editNote(noteData) if !noteEdited { // exit early if the note was not edited - fmt.Println(offline.AnsiError + "Entry is unchanged" + offline.AnsiReset) + fmt.Println(backend.AnsiError + "Entry is unchanged" + backend.AnsiReset) os.Exit(1) } unencryptedEntry = append(nonNoteData, editedNote...) // write and preview the modified entry - writeEntryShortcut(targetLocation, unencryptedEntry, hidePassword) + writeEntryCLI(targetLocation, unencryptedEntry, hidePassword) } // GenUpdate generates a new password for an entry at targetLocation (user input) func GenUpdate(targetLocation string, hidePassword bool) { - // ensure targetLocation exists - offline.TargetIsFile(targetLocation, true, 2) - - // read old entry data - unencryptedEntry := offline.DecryptGPG(targetLocation) + // fetch old entry data + unencryptedEntry := backend.GetOldEntryData(targetLocation, 0) // generate a new password - unencryptedEntry[0] = offline.StringGen(inputInt("Password length:", -1), inputBinary("Generate a complex (special characters) password?"), 0.2) + unencryptedEntry[0] = backend.StringGen(inputInt("Password length:", -1), inputBinary("Generate a complex (special characters) password?"), 0.2) // write and preview the modified entry - writeEntryShortcut(targetLocation, unencryptedEntry, hidePassword) + writeEntryCLI(targetLocation, unencryptedEntry, hidePassword) } // 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 := offline.CreateTempFile() + tempFile := backend.CreateTempFile() defer os.Remove(tempFile.Name()) - editor := offline.ReadConfig([]string{"textEditor"})[0] + editor := backend.ReadConfig([]string{"textEditor"})[0] // write baseNote to tempFile (if it is not empty) if len(baseNote) > 0 { @@ -116,13 +100,13 @@ func editNote(baseNote []string) ([]string, bool) { cmd.Stderr = os.Stderr err := cmd.Run() if err != nil { - panic(offline.AnsiError + "Failed to write note with " + editor + offline.AnsiReset) + panic(backend.AnsiError + "Failed to write note with " + editor + backend.AnsiReset) } // open the tempFile for reading tempFile, err = os.Open(tempFile.Name()) if err != nil { - panic(offline.AnsiError + "Failed to write note with " + editor + offline.AnsiReset) + panic(backend.AnsiError + "Failed to write note with " + editor + backend.AnsiReset) } // read the edited note from the tempFile @@ -136,7 +120,7 @@ func editNote(baseNote []string) ([]string, bool) { tempFile.Close() // remove trailing empty strings from the edited note - note = offline.RemoveTrailingEmptyStrings(note) + note = backend.RemoveTrailingEmptyStrings(note) // trim trailing whitespace from each note line for i, line := range note { diff --git a/src/cli/entryList.go b/src/cli/entryList.go index c8f9ae1..170785e 100644 --- a/src/cli/entryList.go +++ b/src/cli/entryList.go @@ -2,7 +2,7 @@ package cli import ( "fmt" - "github.com/rwinkhart/MUTN/src/offline" + "github.com/rwinkhart/MUTN/src/backend" "io/fs" "os" "path/filepath" @@ -23,10 +23,10 @@ func determineIndentation(skippedDirList []bool, dirList []string, currentDirInd var trimmedDirectory = dirList[currentDirIndex] // determine initial indentation multiplier based on PathSeparator occurrences - indent := strings.Count(trimmedDirectory, offline.PathSeparator) - 1 // subtract 1 to avoid indenting root-level directories + indent := strings.Count(trimmedDirectory, backend.PathSeparator) - 1 // subtract 1 to avoid indenting root-level directories for i, skipped := range skippedDirList[:currentDirIndex] { // checks each skipped directory to determine if it is a parent to the current directory - if strings.HasPrefix(trimmedDirectory, dirList[i]+offline.PathSeparator) { // if the current directory is the child of this iteration's directory... + if strings.HasPrefix(trimmedDirectory, dirList[i]+backend.PathSeparator) { // if the current directory is the child of this iteration's directory... if skipped { // ...and this iteration's directory was skipped... subtractor++ // increment the subtractor to indicate that the visual indentation should be reduced } else { @@ -72,7 +72,7 @@ func printFileEntry(entry string, lastSlash int, charCounter int, colorAlternato } // print fileEntryName to screen - fmt.Printf("%s%s%s ", colorCode, fileEntryName, offline.AnsiReset) + fmt.Printf("%s%s%s ", colorCode, fileEntryName, backend.AnsiReset) return charCounter, colorAlternator } @@ -84,16 +84,16 @@ func EntryListGen() { var dirList []string // walk entry directory - _ = filepath.WalkDir(offline.EntryRoot, + _ = filepath.WalkDir(backend.EntryRoot, func(fullPath string, entry fs.DirEntry, err error) error { // check for errors encountered while walking directory if err != nil { if os.IsNotExist(err) { - fmt.Println(offline.AnsiError + "\nThe entry directory does not exist - run \"mutn init\" to create it" + offline.AnsiReset) + fmt.Println(backend.AnsiError + "\nThe entry directory does not exist - run \"mutn init\" to create it" + backend.AnsiReset) } else { // otherwise, print the source of the error - fmt.Println(offline.AnsiError + "\nAn unexpected error occurred while generating the entry list: " + err.Error() + offline.AnsiReset) + fmt.Println(backend.AnsiError + "\nAn unexpected error occurred while generating the entry list: " + err.Error() + backend.AnsiReset) } os.Exit(1) } @@ -112,7 +112,7 @@ func EntryListGen() { }) // print header bar w/total entry count - fmt.Print("\n"+ansiBlackOnWhite, len(fileList), " libmutton entries:"+offline.AnsiReset) + fmt.Print("\n"+ansiBlackOnWhite, len(fileList), " libmutton entries:"+backend.AnsiReset) // dirList iteration dirListLength := len(dirList) // save length for multiple references below @@ -137,7 +137,7 @@ func EntryListGen() { // check if next directory is within the current one if dirListLength > i+1 { - if nextDir := dirList[i+1]; directory == nextDir[:strings.LastIndex(nextDir, offline.PathSeparator)] { + if nextDir := dirList[i+1]; directory == nextDir[:strings.LastIndex(nextDir, backend.PathSeparator)] { containsSubdirectory = true } else { containsSubdirectory = false @@ -151,7 +151,7 @@ func EntryListGen() { for _, file := range fileList { // print the current file if it belongs in the current directory - otherwise, break the loop and move on to the next directory - if lastSlash := strings.LastIndex(file, offline.PathSeparator) + 1; file[:lastSlash-1] == directory { + if lastSlash := strings.LastIndex(file, backend.PathSeparator) + 1; file[:lastSlash-1] == directory { // print directory header if this is the first run of the loop if !containsFiles { @@ -171,7 +171,7 @@ func EntryListGen() { 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 printDirectoryHeader(vanityDirectory, indent) - fmt.Print(strings.Repeat(" ", indent*2) + ansiEmptyDirectoryWarning + "-empty directory-" + offline.AnsiReset) + fmt.Print(strings.Repeat(" ", indent*2) + ansiEmptyDirectoryWarning + "-empty directory-" + backend.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/entryListUNIX.go b/src/cli/entryListUNIX.go index 3defc57..b2a3a3c 100644 --- a/src/cli/entryListUNIX.go +++ b/src/cli/entryListUNIX.go @@ -4,10 +4,10 @@ package cli import ( "fmt" - "github.com/rwinkhart/MUTN/src/offline" + "github.com/rwinkhart/MUTN/src/backend" "strings" ) func printDirectoryHeader(vanityDirectory string, indent int) { - fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+offline.AnsiReset+"\n", vanityDirectory) + fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+backend.AnsiReset+"\n", vanityDirectory) } diff --git a/src/cli/entryListWIN.go b/src/cli/entryListWIN.go index 85ef420..b5cf6b7 100644 --- a/src/cli/entryListWIN.go +++ b/src/cli/entryListWIN.go @@ -4,10 +4,10 @@ package cli import ( "fmt" - "github.com/rwinkhart/MUTN/src/offline" + "github.com/rwinkhart/MUTN/src/backend" "strings" ) func printDirectoryHeader(vanityDirectory string, indent int) { - fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+offline.AnsiReset+"\n", strings.ReplaceAll(vanityDirectory, offline.PathSeparator, "/")) + fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+backend.AnsiReset+"\n", strings.ReplaceAll(vanityDirectory, backend.PathSeparator, "/")) } diff --git a/src/cli/entryReader.go b/src/cli/entryReader.go index 52333c9..755d21b 100644 --- a/src/cli/entryReader.go +++ b/src/cli/entryReader.go @@ -5,7 +5,7 @@ import ( "os" "strings" - "github.com/rwinkhart/MUTN/src/offline" + "github.com/rwinkhart/MUTN/src/backend" "github.com/charmbracelet/glamour" ) @@ -22,24 +22,24 @@ func EntryReader(decryptedEntry []string, hidePassword bool, sync bool) { // if the first field (password) is not empty, print it if decryptedEntry[0] != "" { if !hidePassword { - fmt.Print(ansiDirectoryHeader + "Password:" + offline.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[0] + offline.AnsiReset + "\n\n") + fmt.Print(ansiDirectoryHeader + "Password:" + backend.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[0] + backend.AnsiReset + "\n\n") } else { - fmt.Print(ansiDirectoryHeader + "Password:" + offline.AnsiReset + "\n" + ansiEmptyDirectoryWarning + "End command in \"show\" or \"-s\" to view" + offline.AnsiReset + "\n\n") + fmt.Print(ansiDirectoryHeader + "Password:" + backend.AnsiReset + "\n" + ansiEmptyDirectoryWarning + "End command in \"show\" or \"-s\" to view" + backend.AnsiReset + "\n\n") } } case 1: // if the second field (username) is not empty, print it if decryptedEntry[1] != "" { - fmt.Print(ansiDirectoryHeader + "Username:" + offline.AnsiReset + "\n" + decryptedEntry[1] + "\n\n") + fmt.Print(ansiDirectoryHeader + "Username:" + backend.AnsiReset + "\n" + decryptedEntry[1] + "\n\n") } case 2: // if the third field (url) is not empty, print it if decryptedEntry[2] != "" { - fmt.Print(ansiDirectoryHeader + "URL:" + offline.AnsiReset + "\n" + decryptedEntry[2] + "\n\n") + fmt.Print(ansiDirectoryHeader + "URL:" + backend.AnsiReset + "\n" + decryptedEntry[2] + "\n\n") } case 3: // print the notes header - fmt.Println(ansiDirectoryHeader + "Notes:" + offline.AnsiReset) + fmt.Println(ansiDirectoryHeader + "Notes:" + backend.AnsiReset) // combine remaining fields into a single string (for markdown rendering) var markdownNotes []string @@ -57,17 +57,17 @@ func EntryReader(decryptedEntry []string, hidePassword bool, sync bool) { } } - if sync && !offline.Windows { + if sync && !backend.Windows { SshypSync() // TODO Remove after native sync is implemented } os.Exit(0) } -// EntryReaderShortcut is a shortcut for EntryReader that decrypts a GPG-encrypted file and prints the contents -func EntryReaderShortcut(targetLocation string, hidePassword bool, sync bool) { - if isFile, _ := offline.TargetIsFile(targetLocation, true, 2); isFile { - EntryReader(offline.DecryptGPG(targetLocation), hidePassword, sync) +// EntryReaderDecrypt is a wrapper for EntryReader that first decrypts a GPG-encrypted file before sending it to EntryReader +func EntryReaderDecrypt(targetLocation string, hidePassword bool, sync bool) { + if isFile, _ := backend.TargetIsFile(targetLocation, true, 2); isFile { + EntryReader(backend.DecryptGPG(targetLocation), hidePassword, sync) } // do not exit, as this is the job of EntryReader } diff --git a/src/cli/init.go b/src/cli/init.go index ae9e297..ebd7752 100644 --- a/src/cli/init.go +++ b/src/cli/init.go @@ -2,7 +2,7 @@ package cli import ( "fmt" - "github.com/rwinkhart/MUTN/src/offline" + "github.com/rwinkhart/MUTN/src/backend" "os" ) @@ -11,20 +11,20 @@ func TempInitCli() { // gpgID var gpgID string if inputBinary("Auto-generate GPG key?") { - gpgID = offline.GpgKeyGen() + gpgID = backend.GpgKeyGen() } else { // select GPG key from menu - uidSlice := offline.GpgUIDListGen() + uidSlice := backend.GpgUIDListGen() gpgIDInt := inputMenuGen("Select GPG key:", uidSlice) if gpgIDInt == 0 { - fmt.Println(offline.AnsiError + "No GPG keys found - please generate one" + offline.AnsiReset) + fmt.Println(backend.AnsiError + "No GPG keys found - please generate one" + backend.AnsiReset) os.Exit(1) } gpgID = uidSlice[gpgIDInt-1] } // textEditor - textEditor := input("Text editor (leave blank for $EDITOR, falls back to \"" + offline.FallbackEditor + "\"):") + textEditor := input("Text editor (leave blank for $EDITOR, falls back to \"" + backend.FallbackEditor + "\"):") - offline.TempInit(map[string]string{"textEditor": textEditor, "gpgID": gpgID}) + backend.TempInit(map[string]string{"textEditor": textEditor, "gpgID": gpgID}) } diff --git a/src/cli/printInfo.go b/src/cli/printInfo.go index 5ef548a..ffaae57 100644 --- a/src/cli/printInfo.go +++ b/src/cli/printInfo.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - "github.com/rwinkhart/MUTN/src/offline" + "github.com/rwinkhart/MUTN/src/backend" ) // global constants used only in this file @@ -14,13 +14,13 @@ const ( ) func HelpMain() { - fmt.Print(ansiBold + "\nMUTN | Copyright (c) 2024 Randall Winkhart\n" + offline.AnsiReset + ` + fmt.Print(ansiBold + "\nMUTN | Copyright (c) 2024 Randall Winkhart\n" + backend.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:" + offline.AnsiReset + ` mutn [/ [argument] [option]] | [argument] +` + ansiBold + "Usage:" + backend.AnsiReset + ` mutn [/ [argument] [option]] | [argument] -` + ansiBold + "Arguments:" + offline.AnsiReset + ` +` + ansiBold + "Arguments:" + backend.AnsiReset + ` help|-h Bring up this menu version|-v Display version and license information init Set up MUTN (generates libmutton.ini) @@ -32,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:" + offline.AnsiReset + ` +` + ansiBold + "Options:" + backend.AnsiReset + ` copy: password|-pw| Copy the password of an entry to your clipboard username|-u Copy the username of an entry to your clipboard @@ -52,17 +52,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:" + offline.AnsiReset + ` You can quickly read an entry with "mutn /" -` + ansiBold + "Tip 2:" + offline.AnsiReset + ` Type "mutn" (no arguments/options) to view a list of saved entries -` + ansiBold + "Tip 3:" + offline.AnsiReset + ` Provide "add", "edit", "copy", or "gen" as the only argument to receive more specific help -` + ansiBold + "Tip 4:" + offline.AnsiReset + " Using \"add\", \"edit\", or \"copy\" without specifying an option (field) will default to \"password\"\n\n") +` + ansiBold + "Tip 1:" + backend.AnsiReset + ` You can quickly read an entry with "mutn /" +` + ansiBold + "Tip 2:" + backend.AnsiReset + ` Type "mutn" (no arguments/options) to view a list of saved entries +` + ansiBold + "Tip 3:" + backend.AnsiReset + ` Provide "add", "edit", "copy", or "gen" as the only argument to receive more specific help +` + ansiBold + "Tip 4:" + backend.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:" + offline.AnsiReset + ` mutn / add