diff --git a/back/1globals.go b/back/1globals.go new file mode 100644 index 0000000..7da53df --- /dev/null +++ b/back/1globals.go @@ -0,0 +1,16 @@ +package back + +import "os" + +var Home, _ = os.UserHomeDir() + +const ( + AnsiError = "\033[38;5;9m" + AnsiReset = "\033[0m" + + ErrorRead = 101 + ErrorWrite = 102 + ErrorTargetNotFound = 105 + ErrorTargetWrongType = 107 + ErrorOther = 111 +) diff --git a/back/cmd.go b/back/cmd.go new file mode 100644 index 0000000..e90f33c --- /dev/null +++ b/back/cmd.go @@ -0,0 +1,32 @@ +package back + +import ( + "bufio" + "io" + "os" + "os/exec" +) + +// WriteToStdin is a utility function that writes a string to a command's stdin. +func WriteToStdin(cmd *exec.Cmd, input string) { + stdin, err := cmd.StdinPipe() + if err != nil { + PrintError("Failed to access stdin for system command: "+err.Error(), ErrorOther, true) + } + + go func() { + defer func(stdin io.WriteCloser) { + _ = stdin.Close() // error ignored; if stdin could be accessed, it can probably be closed + }(stdin) + _, _ = io.WriteString(stdin, input) + }() +} + +// ReadFromStdin is a utility function that reads a string from stdin. +func ReadFromStdin() string { + scanner := bufio.NewScanner(os.Stdin) + if scanner.Scan() { + return scanner.Text() + } + return "" +} diff --git a/back/exitHard.go b/back/exitHard.go new file mode 100644 index 0000000..3c57a46 --- /dev/null +++ b/back/exitHard.go @@ -0,0 +1,10 @@ +//go:build !interactive + +package back + +import "os" + +// Exit (hard) is meant to be used in non-interactive CLI implementations to exit the program after an operation. +func Exit(code int) { + os.Exit(code) +} diff --git a/back/exitSoft.go b/back/exitSoft.go new file mode 100644 index 0000000..81bd2d0 --- /dev/null +++ b/back/exitSoft.go @@ -0,0 +1,8 @@ +//go:build interactive + +package back + +// Exit (soft) is meant to be used in interactive implementations (GUIs/TUIs) to keep the program running after an operation. +func Exit(code int) int { + return code +} diff --git a/back/files.go b/back/files.go new file mode 100644 index 0000000..e6ff9a2 --- /dev/null +++ b/back/files.go @@ -0,0 +1,44 @@ +package back + +import ( + "os" + "strings" +) + +// TargetIsFile checks if the targetLocation is a file, directory, or is inaccessible. +// Requires: failCondition (0 = fail on inaccessible, 1 = fail on inaccessible&file, 2 = fail on inaccessible&directory). +// Returns: isFile, isAccessible. +func TargetIsFile(targetLocation string, errorOnFail bool, failCondition uint8) (bool, bool) { + targetInfo, err := os.Stat(targetLocation) + if err != nil { + if errorOnFail { + PrintError("Failed to access \""+targetLocation+"\" - Ensure it exists and has the correct permissions", ErrorTargetNotFound, true) + } + return false, false + } + if targetInfo.IsDir() { + if errorOnFail && failCondition == 2 { + PrintError("\""+targetLocation+"\" is a directory", ErrorTargetWrongType, true) + } + return false, true + } else { + if errorOnFail && failCondition == 1 { + PrintError("\""+targetLocation+"\" is a file", ErrorTargetWrongType, true) + } + return true, true + } +} + +// CreateTempFile creates a temporary file and returns a pointer to it. +func CreateTempFile() *os.File { + tempFile, err := os.CreateTemp("", "*.markdown") + if err != nil { + PrintError("Failed to create temporary file: "+err.Error(), ErrorWrite, true) + } + return tempFile +} + +// ExpandPathWithHome, given a path (as a string) containing "~", returns the path with "~" expanded to the user's home directory. +func ExpandPathWithHome(path string) string { + return strings.Replace(path, "~", Home, 1) +} diff --git a/back/general.go b/back/general.go new file mode 100644 index 0000000..d4bb6ba --- /dev/null +++ b/back/general.go @@ -0,0 +1,29 @@ +package back + +import ( + "fmt" + "os" +) + +// RemoveTrailingEmptyStrings removes empty strings from the end of a slice. +func RemoveTrailingEmptyStrings(slice []string) []string { + for i := len(slice) - 1; i >= 0; i-- { + if slice[i] != "" { + return slice[:i+1] + } + } + return nil +} + +// PrintError prints an error message in the standard libmutton format and exits with the specified exit code. +// Requires: message (the error message to print), +// exitCode (the exit code to use), +// forceHardExit (if true, exit immediately; if false, allow soft exit for interactive clients). +func PrintError(message string, exitCode int, forceHardExit bool) { + fmt.Println(AnsiError + message + AnsiReset) + if forceHardExit { + os.Exit(exitCode) + } else { + Exit(exitCode) + } +} diff --git a/front/input.go b/front/input.go new file mode 100644 index 0000000..4b9fdf8 --- /dev/null +++ b/front/input.go @@ -0,0 +1,58 @@ +package front + +import ( + "bufio" + "fmt" + "os" + "strings" + + "golang.org/x/term" +) + +// Input prompts the user for input and returns the input as a string. +func Input(prompt string) string { + fmt.Print("\n" + prompt + " ") + reader := bufio.NewReader(os.Stdin) + userInput, _ := reader.ReadString('\n') + return strings.TrimRight(userInput, "\n\r ") // remove trailing newlines, carriage returns, and spaces +} + +// InputHidden prompts the user for input and returns the input as a byte array, hiding the input from the terminal. +func InputHidden(prompt string) []byte { + fmt.Print("\n" + prompt + " ") + byteInput, _ := term.ReadPassword(int(os.Stdin.Fd())) + fmt.Println() + return byteInput +} + +// InputBinary prompts the user with a yes/no question and returns the response as a boolean. +func InputBinary(prompt string) bool { + reader := bufio.NewReader(os.Stdin) + fmt.Print("\n" + prompt + " (y/N) ") + char, _, _ := reader.ReadRune() + if char == 'y' || char == 'Y' { + return true + } + return false +} + +// InputPositiveInt prompts the user for input and returns the input as an integer. +// A negative min/max value will disable the respective limit. +func InputPositiveInt(prompt string, min, max int) int { + for { + fmt.Print("\n" + prompt + " ") + var userInput int + _, err := fmt.Scanln(&userInput) + if err == nil && (userInput >= min || min < 0) && (userInput <= max || max < 0) { + return userInput + } + } +} + +// InputMenuGen prompts the user with a menu and returns the user's choice as an integer. +func InputMenuGen(prompt string, options []string) int { + for i, option := range options { + fmt.Printf("%d. %s\n", i+1, option) + } + return InputPositiveInt(prompt, 1, len(options)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d6d591b --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module github.com/rwinkhart/go-boilerplate + +go 1.24.3 + +require golang.org/x/term v0.32.0 + +require golang.org/x/sys v0.33.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0c9fbb7 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=