diff --git a/commit.sh b/commit.sh index 751a82e..729c383 100755 --- a/commit.sh +++ b/commit.sh @@ -1,5 +1,6 @@ #!/bin/sh -gofmt -l -w -s ./src/*.go -git add -f extra src/*.go src/*.mod .gitignore commit.sh LICENSE README.md +gofmt -l -w -s ./src/cli/*.go +gofmt -l -w -s ./src/offline/*.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/go.mod b/go.mod new file mode 100644 index 0000000..7d598a0 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/rwinkhart/MUTN + +go 1.22.0 + +require golang.org/x/crypto v0.20.0 + +require ( + golang.org/x/sys v0.17.0 // indirect + golang.org/x/term v0.17.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6675135 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= diff --git a/src/cli/ALLglobals.go b/src/cli/ALLglobals.go new file mode 100644 index 0000000..9c453c9 --- /dev/null +++ b/src/cli/ALLglobals.go @@ -0,0 +1,20 @@ +package cli + +import ( + "github.com/rwinkhart/MUTN/src/offline" + termy "golang.org/x/crypto/ssh/terminal" + "os" +) + +// global variables used across multiple files +var ( + rootLength = len(offline.EntryRoot) + width, _, _ = termy.GetSize(int(os.Stdout.Fd())) +) + +// global constants used across multiple files +const ( + ansiReset = "\033[0m" + ansiBold = "\033[1m" + ansiBlackOnWhite = "\033[38;5;0;48;5;15m" +) diff --git a/src/cli/entryList.go b/src/cli/entryList.go new file mode 100644 index 0000000..6d74e3b --- /dev/null +++ b/src/cli/entryList.go @@ -0,0 +1,194 @@ +package cli + +import ( + "fmt" + "github.com/rwinkhart/MUTN/src/offline" + "io/fs" + "os" + "path/filepath" + "strings" +) + +// global constants used only in this file +const ( + ansiAlternateEntryColor = "\033[38;5;8m" +) + +// calculates and returns the final visual indentation multiplier (needed to adjust indentation for skipped parent directories) - also subtracts "old" text from directory header +func indentSubtractor(skippedDirList []bool, dirList []string, currentDirIndex int, indent int) (int, string) { + var subtractor int // tracks how much to subtract from expected indentation multiplier + var lastPrefixIndex int // tracks the index (in both skippedDirList and dirList) of the last displayed parent directory + var trimmedDirectory = dirList[currentDirIndex] + + 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 skipped { // ...and this iteration's directory was skipped... + subtractor++ // increment the subtractor to indicate that the visual indentation should be reduced + } else { + lastPrefixIndex = i + } + } + } + + indent = indent - subtractor // calculates final visual indentation multiplier + + if indent < 0 { // disallow negative indentation multipliers + indent = 0 + } else if indent > 0 { // trim the most recently displayed parent directory from the directory header to avoid displaying redundant information + trimmedDirectory = strings.Replace(trimmedDirectory, dirList[lastPrefixIndex], "", 1) + } + + return indent, trimmedDirectory +} + +// processing for printing file entries (determines color, line wrapping, and prints) +func printFileEntry(entry string, lastSlash int, charCounter int, colorAlternator int8, indent int) (int, int8) { + // determine color to print fileEntryName (alternate each time function is run) + var colorCode string + if colorAlternator > 0 { + colorCode = "" + } else { + colorCode = ansiAlternateEntryColor + } + colorAlternator = -colorAlternator + + // trim the containing directory and file extension from the entry to determine fileEntryName + fileEntryName := entry[lastSlash:] + fileEntryName = fileEntryName[:len(fileEntryName)-4] + + if charCounter == 0 { // indent first line of entries for each directory header + fmt.Print(strings.Repeat(" ", indent*2)) + } + + // determine whether to wrap to a new line (+1 is to account for trailing spaces) + charCounter += len(fileEntryName) + 1 + if indentation := indent * 2; charCounter+(indentation) >= width { + charCounter = len(fileEntryName) + 1 + fmt.Print("\n" + strings.Repeat(" ", indentation)) // indent each line + } + + // print fileEntryName to screen + fmt.Printf("%s%s%s ", colorCode, fileEntryName, ansiReset) + + return charCounter, colorAlternator +} + +// EntryListGen generates and displays full libmutton entry list +func EntryListGen() { + fmt.Print("\n" + ansiBlackOnWhite + "libmutton entries:" + ansiReset) + + // define file/directory containing slices so that they may be accessed by the anonymous WalkDir function + var fileList []string + var dirList []string + + // walk entry directory + _ = filepath.WalkDir(offline.EntryRoot, + func(fullPath string, entry fs.DirEntry, err error) error { + + // check for errors encountered while walking directory + if err != nil { + // create EntryRoot if the error is the result of it not existing on the system + if os.IsNotExist(err) { + _ = os.Mkdir(offline.EntryRoot, 0700) + dirList = append(dirList, "") + } else { + // otherwise, print the source of the error + fmt.Print("\n\n\033[38;5;9mAn unexpected error occurred while generating the entry list: " + err.Error() + ansiReset) + } + // quit walking EntryRoot and return nil to allow the program to continue + return nil + } + + // trim root path from each path before storing + trimmedPath := fullPath[rootLength:] + + // create three separate slices for root-level entries, all other entries, and all subdirectories + // root-level entries get their own slice so that they can be alphabetically sorted without the chance of directories being placed in from of them + if !entry.IsDir() { + fileList = append(fileList, trimmedPath) + } else { + dirList = append(dirList, trimmedPath) + } + + return nil + }) + + // dirList iteration + dirListLength := len(dirList) // save length for multiple references below + var skippedDirList = make([]bool, dirListLength) // stores whether each directory was skipped during printout (later used to determine appropriate visual indentation) + charCounter := 0 // track whether to line-wrap based on character count in line + var colorAlternator int8 = 1 // track alternating colors for each printed entry name + var containsSubdirectory bool // indicates whether the current directory contains a subdirectory + var indent int // visual indentation multiplier + var vanityDirectory string // directory header printed to end-user - visual only, not used in any processing + for i, directory := range dirList { + + // reset formatting variables for new directory + charCounter = 0 + colorAlternator = 1 + + // default to assuming this directory will be skipped (unless it is the root) + if i == 0 { + skippedDirList[i] = false + } else { + skippedDirList[i] = true + } + + // determine directory's indentation multiplier based on PathSeparator occurrences + indent = strings.Count(directory, offline.PathSeparator) - 1 // subtract 1 to avoid indenting root-level directories + + // 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)] { + containsSubdirectory = true + } else { + containsSubdirectory = false + } + } else { + containsSubdirectory = false + } + + // fileList iteration + containsFiles := false // indicates whether the current directory contains files (entries) + 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 { + + // print directory header if this is the first run of the loop + if !containsFiles { + containsFiles = true + skippedDirList[i] = false // the directory header is being printed, indicate that it is not being skipped + indent, vanityDirectory = indentSubtractor(skippedDirList, dirList, i, indent) // calculate the final indentation multiplier + if !offline.Windows { // for consistency, format directories with UNIX-style path separators on all platforms + fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+"\033[38;5;7;48;5;8m%s/"+ansiReset+"\n", vanityDirectory) + } else { + fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+"\033[38;5;7;48;5;8m%s/"+ansiReset+"\n", strings.ReplaceAll(vanityDirectory, offline.PathSeparator, "/")) + } + } + + charCounter, colorAlternator = printFileEntry(file, lastSlash, charCounter, colorAlternator, indent) + } + } + + if !containsFiles { // if the current directory contains no files... + if !containsSubdirectory { // nor does it contain any subdirectories... + 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 = indentSubtractor(skippedDirList, dirList, i, indent) // calculate the final indentation multiplier + if !offline.Windows { // for consistency, format directories with UNIX-style path separators on all platforms + fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+"\033[38;5;7;48;5;8m%s/"+ansiReset+"\n", vanityDirectory) + } else { + fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+"\033[38;5;7;48;5;8m%s/"+ansiReset+"\n", strings.ReplaceAll(vanityDirectory, offline.PathSeparator, "/")) + } + fmt.Print(strings.Repeat(" ", indent*2) + "\033[38;5;11m-empty directory-" + 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\".") + } + } + } + } + + // print trailing new lines for proper spacing after entry list is complete + fmt.Print("\n\n") +} diff --git a/src/cli/printInfo.go b/src/cli/printInfo.go new file mode 100644 index 0000000..dc4dc4b --- /dev/null +++ b/src/cli/printInfo.go @@ -0,0 +1,141 @@ +package cli + +import "fmt" + +// global constants used only in this file +const ( + ansiGoFuchsia = "\033[38;2;206;48;98m" + ansiGoGopher = "\033[38;2;1;173;216m" +) + +func HelpMain() { + fmt.Print(ansiBold + "\nMUTN | Copyright (c) 2024 Randall Winkhart\n" + 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:" + ansiReset + ` mutn [/ [argument] [option]] | [argument] + +` + ansiBold + "Arguments:" + ansiReset + ` + help/--help/-h Bring up this menu + version/-v Display version and license information + init Set up MUTN + tweak Change configuration options + add Add an entry + gen Generate a new password + edit Edit an existing entry + copy Copy details of an entry to your clipboard + shear Delete an existing entry + sync Manually sync the entry directory + +` + ansiBold + "Options:" + ansiReset + ` + add: + password/-p Add a password entry + note/-n Add a note entry + folder/-f Add a new folder for entries + edit: + rename/relocate/-r Rename or relocate an entry + username/-u Change the username of an entry + password/-p Change the password of an entry + url/-l Change the url attached to an entry + note/-n Change the note attached to an entry + copy: + username/-u Copy the username of an entry to your clipboard + password/-p Copy the password of an entry to your clipboard + url/-l Copy the url of an entry to your clipboard + note/-n Copy the note of an entry to your clipboard + gen: + update/-u Generate a password for an existing entry + +` + ansiBold + "Tip 1:" + ansiReset + ` You can quickly read an entry with "mutn /" +` + ansiBold + "Tip 2:" + ansiReset + ` Type "mutn" (no arguments/options) to view a list of saved entries +` + ansiBold + "Tip 3:" + ansiReset + " Provide \"add\", \"edit\", \"copy\", or \"gen\" as the only argument to receive more specific help\n\n") +} + +func HelpAdd() { + fmt.Print(ansiBold + "\nUsage:" + ansiReset + ` mutn / add