Use go-boilerplate

This commit is contained in:
2025-05-09 16:12:03 +00:00
parent aec42cad96
commit 03c040b373
22 changed files with 130 additions and 224 deletions
+1 -16
View File
@@ -1,14 +1,9 @@
package core
import (
"os"
)
type ByteInputFetcher func(prompt string) []byte
var (
PassphraseInputFunction ByteInputFetcher // Clients should set this to a function that fetches hidden input from the user
Home, _ = os.UserHomeDir()
GetPassphrase ByteInputFetcher // Clients should set this to a function that fetches hidden input from the user
)
const (
@@ -18,20 +13,10 @@ const (
FSPath = "\u259e" // ▞ Path separator
FSMisc = "\u259f" // ▟ Misc. field separator (if \u259d is already used)
AnsiError = "\033[38;5;9m"
AnsiReset = "\033[0m"
ErrorRead = 101
ErrorWrite = 102
ErrorSyncProcess = 103
ErrorServerConnection = 104
ErrorTargetNotFound = 105
ErrorTargetExists = 106
ErrorTargetWrongType = 107
ErrorDecryption = 108
ErrorEncryption = 109
ErrorClipboard = 110
ErrorOther = 111
)
var GetPassphrase func() []byte
+5 -3
View File
@@ -2,10 +2,12 @@
package core
import "github.com/rwinkhart/go-boilerplate/back"
var (
EntryRoot = Home + "/.local/share/libmutton" // Path to libmutton entry directory
ConfigDir = Home + "/.config/libmutton" // Path to libmutton configuration directory
ConfigPath = ConfigDir + "/libmutton.ini" // Path to libmutton configuration file
EntryRoot = back.Home + "/.local/share/libmutton" // Path to libmutton entry directory
ConfigDir = back.Home + "/.config/libmutton" // Path to libmutton configuration directory
ConfigPath = ConfigDir + "/libmutton.ini" // Path to libmutton configuration file
)
const (
+5 -3
View File
@@ -5,6 +5,8 @@ package core
import (
"strings"
"time"
"github.com/rwinkhart/go-boilerplate/back"
)
// clipClearProcess clears the clipboard after 30 seconds if the clipboard contents have not changed.
@@ -15,9 +17,9 @@ func clipClearProcess(assignedContents string) {
clearClipboard := func() {
err := cmdClear.Run()
if err != nil {
PrintError("Failed to clear clipboard", ErrorClipboard, true)
back.PrintError("Failed to clear clipboard", ErrorClipboard, true)
}
Exit(0)
back.Exit(0)
}
// if assignedContents is empty, clear the clipboard immediately and unconditionally
@@ -31,7 +33,7 @@ func clipClearProcess(assignedContents string) {
newContents, err := cmdPaste.Output()
if err != nil {
PrintError("Failed to read clipboard contents", ErrorClipboard, true)
back.PrintError("Failed to read clipboard contents", ErrorClipboard, true)
}
if assignedContents == strings.TrimRight(string(newContents), "\r\n") {
+6 -5
View File
@@ -5,6 +5,7 @@ import (
"io/fs"
"os"
"github.com/rwinkhart/go-boilerplate/back"
"gopkg.in/ini.v1"
)
@@ -13,7 +14,7 @@ import (
func loadConfig() *ini.File {
cfg, err := ini.Load(ConfigPath)
if err != nil {
PrintError("Failed to load libmutton.ini: "+err.Error(), ErrorRead, true)
back.PrintError("Failed to load libmutton.ini: "+err.Error(), back.ErrorRead, true)
}
return cfg
}
@@ -38,11 +39,11 @@ func ParseConfig(valuesRequested [][2]string, missingValueError string) ([]strin
case "":
err = fmt.Errorf("Failed to find value for key \"%s\" in section \"[%s]\" in libmutton.ini", pair[1], pair[0])
case "0":
Exit(0) // hard (expected) exit for CLI; GUI/TUI continue silently
back.Exit(0) // hard (expected) exit for CLI; GUI/TUI continue silently
default:
err = fmt.Errorf("%s", missingValueError)
}
PrintError(err.Error(), ErrorRead, false)
back.PrintError(err.Error(), back.ErrorRead, false)
// if interactive (soft exit), return nil and the error to be handled by the caller
return nil, err
}
@@ -60,7 +61,7 @@ func GenDeviceIDList(errorOnFail bool) *[]fs.DirEntry {
deviceIDList, err := os.ReadDir(ConfigDir + PathSeparator + "devices")
if err != nil {
if errorOnFail {
PrintError("Failed to read the devices directory: "+err.Error(), ErrorRead, true)
back.PrintError("Failed to read the devices directory: "+err.Error(), back.ErrorRead, true)
} else {
return nil // a nil return value indicates that the devices directory could not be read/does not exist
}
@@ -109,6 +110,6 @@ func WriteConfig(valuesToWrite [][3]string, keysToPrune [][2]string, append bool
// save to libmutton.ini
err := cfg.SaveTo(ConfigPath)
if err != nil {
PrintError("Failed to save libmutton.ini: "+err.Error(), ErrorWrite, true)
back.PrintError("Failed to save libmutton.ini: "+err.Error(), back.ErrorWrite, true)
}
}
+6 -5
View File
@@ -8,11 +8,12 @@ import (
steamtotp "github.com/fortis/go-steam-totp"
"github.com/pquerna/otp/totp"
"github.com/rwinkhart/go-boilerplate/back"
)
// CopyArgument copies a field from an entry to the clipboard.
func CopyArgument(targetLocation string, field int) {
if isFile, _ := TargetIsFile(targetLocation, true, 2); isFile {
if isFile, _ := back.TargetIsFile(targetLocation, true, 2); isFile {
decryptedEntry := DecryptFileToSlice(targetLocation)
var copySubject string // will store data to be copied
@@ -22,7 +23,7 @@ func CopyArgument(targetLocation string, field int) {
// ensure field is not empty
if decryptedEntry[field] == "" {
PrintError("Field is empty", ErrorTargetNotFound, true)
back.PrintError("Field is empty", back.ErrorTargetNotFound, true)
}
if field != 2 {
@@ -48,7 +49,7 @@ func CopyArgument(targetLocation string, field int) {
}
}
} else {
PrintError("Field does not exist in entry", ErrorTargetNotFound, true)
back.PrintError("Field does not exist in entry", back.ErrorTargetNotFound, true)
}
// copy field to clipboard, launch clipboard clearing process
@@ -58,7 +59,7 @@ func CopyArgument(targetLocation string, field int) {
// ClipClearArgument reads the assigned clipboard contents from stdin and passes them to clipClearProcess.
func ClipClearArgument() {
assignedContents := readFromStdin()
assignedContents := back.ReadFromStdin()
if assignedContents == "" {
os.Exit(0) // use os.Exit instead of core.Exit, as this function runs out of a background subprocess that is invisible to the user (will never appear in GUI/TUI environment)
}
@@ -77,7 +78,7 @@ func GenTOTP(secret string, time time.Time, forSteam bool) string {
}
if err != nil {
PrintError("Error generating TOTP code", ErrorOther, true)
back.PrintError("Error generating TOTP code", back.ErrorOther, true)
}
return totpToken
+5 -3
View File
@@ -5,6 +5,8 @@ package core
import (
"os"
"os/exec"
"github.com/rwinkhart/go-boilerplate/back"
)
// copyString copies a string to the clipboard.
@@ -18,13 +20,13 @@ func copyString(continuous bool, copySubject string) {
} else if _, envSet = os.LookupEnv("DISPLAY"); envSet {
cmdCopy = exec.Command("xclip", "-sel", "c", "-t", "text/plain")
} else {
PrintError("Clipboard platform could not be determined", ErrorClipboard, true)
back.PrintError("Clipboard platform could not be determined", ErrorClipboard, true)
}
writeToStdin(cmdCopy, copySubject)
back.WriteToStdin(cmdCopy, copySubject)
err := cmdCopy.Run()
if err != nil {
PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true)
back.PrintError("Failed to copy to clipboard: "+err.Error(), ErrorClipboard, true)
}
if !continuous {
+3 -1
View File
@@ -1,9 +1,11 @@
package core
import "github.com/rwinkhart/go-boilerplate/back"
// GetOldEntryData decrypts and returns old entry data (with all required lines present).
func GetOldEntryData(targetLocation string, field int) []string {
// ensure targetLocation exists
TargetIsFile(targetLocation, true, 2)
back.TargetIsFile(targetLocation, true, 2)
// read old entry data
unencryptedEntry := DecryptFileToSlice(targetLocation)
-10
View File
@@ -1,10 +0,0 @@
//go:build !interactive
package core
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)
}
-8
View File
@@ -1,8 +0,0 @@
//go:build interactive
package core
// 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
}
+6 -5
View File
@@ -3,6 +3,7 @@ package core
import (
"os"
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/rcw/wrappers"
)
@@ -10,7 +11,7 @@ import (
func RCWSanityCheckGen(passphrase []byte) {
err := wrappers.GenSanityCheck(ConfigDir+PathSeparator+"sanity.rcw", passphrase)
if err != nil {
PrintError("Failed to generate sanity check file: "+err.Error(), ErrorWrite, true)
back.PrintError("Failed to generate sanity check file: "+err.Error(), back.ErrorWrite, true)
}
}
@@ -20,7 +21,7 @@ func DirInit(preserveOldConfigDir bool) string {
// create EntryRoot
err := os.MkdirAll(EntryRoot, 0700)
if err != nil {
PrintError("Failed to create \""+EntryRoot+"\": "+err.Error(), ErrorWrite, true)
back.PrintError("Failed to create \""+EntryRoot+"\": "+err.Error(), back.ErrorWrite, true)
}
// get old device ID before its potential removal
@@ -28,11 +29,11 @@ func DirInit(preserveOldConfigDir bool) string {
// remove existing config directory (if it exists and not in append mode)
if !preserveOldConfigDir {
_, isAccessible := TargetIsFile(ConfigDir, false, 1)
_, isAccessible := back.TargetIsFile(ConfigDir, false, 1)
if isAccessible {
err = os.RemoveAll(ConfigDir)
if err != nil {
PrintError("Failed to remove existing config directory: "+err.Error(), ErrorWrite, true)
back.PrintError("Failed to remove existing config directory: "+err.Error(), back.ErrorWrite, true)
}
}
}
@@ -40,7 +41,7 @@ func DirInit(preserveOldConfigDir bool) string {
// create config directory w/devices subdirectory
err = os.MkdirAll(ConfigDir+PathSeparator+"devices", 0700)
if err != nil {
PrintError("Failed to create \""+ConfigDir+"\": "+err.Error(), ErrorWrite, true)
back.PrintError("Failed to create \""+ConfigDir+"\": "+err.Error(), back.ErrorWrite, true)
}
return oldDeviceID
+3 -1
View File
@@ -6,13 +6,15 @@ import (
"os"
"os/exec"
"strconv"
"github.com/rwinkhart/go-boilerplate/back"
)
// LaunchClipClearProcess launches the timed clipboard clearing process.
// For non-interactive CLI implementations, an entirely separate process is created for this purpose.
func LaunchClipClearProcess(copySubject string, isWayland bool) {
cmd := exec.Command(os.Args[0], "clipclear", strconv.FormatBool(isWayland))
writeToStdin(cmd, copySubject)
back.WriteToStdin(cmd, copySubject)
cmd.Start()
os.Exit(0) // use os.Exit directly since this version of this function is only meant for non-interactive CLI implementations
}
+7 -6
View File
@@ -6,13 +6,14 @@ import (
"os/exec"
"strings"
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/rcw/daemon"
"github.com/rwinkhart/rcw/wrappers"
)
// RCWDArgument reads the passphrase from stdin and caches it via an RCW daemon.
func RCWDArgument() {
passphrase := readFromStdin()
passphrase := back.ReadFromStdin()
if passphrase == "" {
os.Exit(0)
}
@@ -24,7 +25,7 @@ func DecryptFileToSlice(targetLocation string) []string {
// read encrypted file
encBytes, err := os.ReadFile(targetLocation)
if err != nil {
PrintError("Failed to open \""+targetLocation+"\" for decryption - "+err.Error(), ErrorDecryption, true)
back.PrintError("Failed to open \""+targetLocation+"\" for decryption - "+err.Error(), back.ErrorRead, true)
}
// decrypt data using RCW daemon
@@ -37,7 +38,7 @@ func DecryptFileToSlice(targetLocation string) []string {
// directly to avoid waiting for socket file creation
decBytes, err := wrappers.Decrypt(encBytes, passphrase)
if err != nil {
PrintError("Failed to decrypt \""+targetLocation+"\" - "+err.Error(), ErrorDecryption, true)
back.PrintError("Failed to decrypt \""+targetLocation+"\" - "+err.Error(), ErrorDecryption, true)
}
return strings.Split(string(decBytes), "\n")
}
@@ -62,15 +63,15 @@ func launchRCWDProcess() []byte {
}
var passphrase []byte
for {
passphrase = GetPassphrase()
passphrase = GetPassphrase("RCW Passphrase:")
err := wrappers.RunSanityCheck(ConfigDir+PathSeparator+"sanity.rcw", passphrase)
if err == nil {
break
}
fmt.Println(AnsiError + "Incorrect passphrase" + AnsiReset)
fmt.Println(back.AnsiError + "Incorrect passphrase" + back.AnsiReset)
}
cmd := exec.Command(os.Args[0], "startrcwd")
writeToStdin(cmd, string(passphrase))
back.WriteToStdin(cmd, string(passphrase))
cmd.Start()
return passphrase
+7 -93
View File
@@ -1,93 +1,25 @@
package core
import (
"bufio"
"crypto/rand"
"fmt"
"io"
"math"
"math/big"
"os"
"os/exec"
"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
}
}
"github.com/rwinkhart/go-boilerplate/back"
)
// WriteEntry writes entryData to an encrypted file at targetLocation.
func WriteEntry(targetLocation string, entryData []byte) {
encryptedBytes := EncryptBytes(entryData)
err := os.WriteFile(targetLocation, encryptedBytes, 0600)
if err != nil {
PrintError("Failed to write to file: "+err.Error(), ErrorWrite, true)
back.PrintError("Failed to write to file: "+err.Error(), back.ErrorWrite, true)
}
}
// 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 ""
}
// 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
}
// 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
}
// ClampTrailingWhitespace strips trailing newlines, carriage returns, and tabs from each line in a note.
// Additionally, it removes single trailing spaces and truncates multiple trailing spaces to two (for Markdown formatting).
func ClampTrailingWhitespace(note []string) {
@@ -123,16 +55,16 @@ func ClampTrailingWhitespace(note []string) {
// Returns: statusCode (0 = success, 1 = target location already exists, 2 = containing directory is invalid).
func EntryAddPrecheck(targetLocation string) uint8 {
// ensure target location does not already exist
_, isAccessible := TargetIsFile(targetLocation, false, 0)
_, isAccessible := back.TargetIsFile(targetLocation, false, 0)
if isAccessible {
PrintError("Target location already exists", ErrorTargetExists, false)
back.PrintError("Target location already exists", ErrorTargetExists, false)
return 1 // inform interactive clients that the target location already exists
}
// ensure target containing directory exists and is a directory (not a file)
containingDir := targetLocation[:strings.LastIndex(targetLocation, PathSeparator)]
isFile, isAccisAccessible := TargetIsFile(containingDir, false, 1)
isFile, isAccisAccessible := back.TargetIsFile(containingDir, false, 1)
if isFile || !isAccisAccessible {
PrintError("\""+containingDir+"\" is not a valid containing directory", ErrorTargetWrongType, false)
back.PrintError("\""+containingDir+"\" is not a valid containing directory", back.ErrorTargetWrongType, false)
return 2 // inform interactive clients that the containing directory is invalid
}
return 0
@@ -205,21 +137,3 @@ func EntryIsNotEmpty(entryData []string) bool {
}
return false
}
// 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)
}
// 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)
}
}