From 41b4c4b6ca2f5a74899e87c6d520df7b140671b3 Mon Sep 17 00:00:00 2001 From: Randall Winkhart Date: Sun, 9 Nov 2025 23:35:48 -0500 Subject: [PATCH] Initial interactive copy menu --- docs/man | 3 +- go.mod | 2 +- go.sum | 4 +-- mutn.go | 9 +++-- src/cli/copyMenu.go | 82 ++++++++++++++++++++++++++++++++++++++++++++ src/cli/printInfo.go | 1 + 6 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 src/cli/copyMenu.go diff --git a/docs/man b/docs/man index 9c919dd..8f4f7df 100644 --- a/docs/man +++ b/docs/man @@ -1,4 +1,4 @@ -.TH MUTN 1 "17 August 2025" "v0.3.0" "MUTN man page" +.TH MUTN 1 "09 November 2025" "v0.3.B" "MUTN man page" .SH NAME \fBmutn\fR - Simple, self-hosted, SSH-synchronized password and note management based on libmutton. It is the successor to sshyp. @@ -63,6 +63,7 @@ copy: totp|-t Generate and copy the TOTP token for an entry to your clipboard url|-l Copy the URL in an entry to your clipboard note|-n Copy the first note line in an entry to your clipboard + menu|-m Open interactive menu to copy any combination of fields edit: password|-pw Change the password in an entry diff --git a/go.mod b/go.mod index 032e972..5266aea 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.25.4 require ( github.com/charmbracelet/glamour v0.7.0 github.com/rwinkhart/go-boilerplate v0.1.0 - github.com/rwinkhart/libmutton v0.4.2 + github.com/rwinkhart/libmutton v0.4.3-0.20251110042347-bf1399ce8d6e golang.org/x/term v0.36.0 ) diff --git a/go.sum b/go.sum index 3565a0f..e86a325 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/rwinkhart/go-highlite v0.1.1 h1:9TxbRhYVfD/3YaEgNk1BtEiZ0t8/5mxDUZqNM github.com/rwinkhart/go-highlite v0.1.1/go.mod h1:mWLMtCWcyV0BG4NeyPyAUGMjPvI0ds6vXmUq89QwBFA= github.com/rwinkhart/go-winio v0.1.0 h1:b72agLW+dETGmhR3VbcbwnStfgKfc5AfgJOXBJDkaHg= github.com/rwinkhart/go-winio v0.1.0/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= -github.com/rwinkhart/libmutton v0.4.2 h1:JYN61KDG8K1Q5OvKdVJIibrDHxDffUPl3AgpN0JrYV8= -github.com/rwinkhart/libmutton v0.4.2/go.mod h1:ZXDCjk1Wec5UJK8ID9AF+jqicWoZmMGzuaPw1Unu3HQ= +github.com/rwinkhart/libmutton v0.4.3-0.20251110042347-bf1399ce8d6e h1:AL7cSPkdiv69uFrBCMJAQP1PPG0LN1mBeOFqQuF+j7k= +github.com/rwinkhart/libmutton v0.4.3-0.20251110042347-bf1399ce8d6e/go.mod h1:ZXDCjk1Wec5UJK8ID9AF+jqicWoZmMGzuaPw1Unu3HQ= github.com/rwinkhart/peercred-mini v0.1.2 h1:4cGWDbv0whvLeVvbUdx84V/9p+2fS+DEXgrA1KxlRFo= github.com/rwinkhart/peercred-mini v0.1.2/go.mod h1:LLHG7YshHEpbpJJP+Il9nx2dnGj5O3VGE32rWmflj0c= github.com/rwinkhart/rcw v0.2.3 h1:g1rVaspZnZM8nWupeSdVO827di4ORMZnczw6iXyo01Y= diff --git a/mutn.go b/mutn.go index 61011b4..6c8b885 100644 --- a/mutn.go +++ b/mutn.go @@ -12,6 +12,7 @@ import ( "github.com/rwinkhart/go-boilerplate/front" "github.com/rwinkhart/go-boilerplate/other" "github.com/rwinkhart/libmutton/cfg" + "github.com/rwinkhart/libmutton/clip" "github.com/rwinkhart/libmutton/core" "github.com/rwinkhart/libmutton/crypt" "github.com/rwinkhart/libmutton/global" @@ -47,7 +48,7 @@ func main() { case "show", "-s": cli.EntryReaderDecrypt(targetLocation, false) case "copy": - err := core.CopyArgument(targetLocation, 0) + err := clip.CopyArgument(targetLocation, 0) if err != nil { other.PrintError("Failed to copy password to clipboard: "+err.Error(), global.ErrorClipboard) } @@ -94,10 +95,12 @@ func main() { field = 3 case "note", "-n": field = 4 + case "menu", "-m": + cli.CopyMenu(targetLocation) default: cli.HelpCopy() } - err := core.CopyArgument(targetLocation, field) + err := clip.CopyArgument(targetLocation, field) if err != nil { other.PrintError("Failed to copy field to clipboard: "+err.Error(), global.ErrorClipboard) } @@ -182,7 +185,7 @@ func main() { } else { switch args[1] { case "clipclear": - err := core.ClipClearArgument() + err := clip.ClipClearArgument() if err != nil { other.PrintError("Failure occurred in clipboard clearing process: "+err.Error(), global.ErrorClipboard) } diff --git a/src/cli/copyMenu.go b/src/cli/copyMenu.go new file mode 100644 index 0000000..2c54428 --- /dev/null +++ b/src/cli/copyMenu.go @@ -0,0 +1,82 @@ +package cli + +import ( + "fmt" + + "github.com/rwinkhart/go-boilerplate/back" + "github.com/rwinkhart/go-boilerplate/front" + "github.com/rwinkhart/go-boilerplate/other" + "github.com/rwinkhart/libmutton/clip" + "github.com/rwinkhart/libmutton/crypt" + "github.com/rwinkhart/libmutton/global" + "github.com/rwinkhart/libmutton/synccommon" +) + +// CopyMenu decrypts an entry and allows the user to +// interactively copy fields without having to re-decrypt each time. +func CopyMenu(targetLocation string) { + // decrypt entry + decSlice, err := crypt.DecryptFileToSlice(targetLocation) + if err != nil { + other.PrintError("Failed to decrypt entry: "+err.Error(), global.ErrorDecryption) + } + + // determine populated fields in entry + fieldIndexToString := map[int]string{ + 0: "Password", + 1: "Username", + 2: "TOTP Code", + 3: "URL", + 4: "Note (first line)", + } + var fields []string + for i, _ := range decSlice[:5] { + if decSlice[i] != "" { + fields = append(fields, fieldIndexToString[i]) + } + } + + // copy selected field to clipboard + for { + choice := front.InputMenuGen("Field to copy:", fields) + switch fields[choice-1] { + case "Password": + choice = 0 + case "Username": + choice = 1 + case "TOTP Code": + choice = 2 + case "URL": + choice = 3 + case "Note (first line)": + choice = 4 + } + if choice == 2 { + fmt.Println(ansiEmptyDirectoryWarning + "[Starting]" + back.AnsiReset + " TOTP clipboard refresher") + errorChan := make(chan error) + done := make(chan bool) + go clip.TOTPCopier(decSlice[2], 0, errorChan, done) + // block until first successful copy + err = <-errorChan + if err != nil { + other.PrintError("Failed to copy TOTP token: "+err.Error(), global.ErrorClipboard) + } + fmt.Println(synccommon.AnsiDownload + "[Started]" + back.AnsiReset + " TOTP clipboard refresher") + // block until the user sends "q" + for { + input := front.Input("Service will run until 'q' is entered:") + if input == "q" { + break + } + } + close(done) + fmt.Print(synccommon.AnsiUpload + "\n[Stopped]" + back.AnsiReset + " TOTP clipboard refresher\n\n") + } else { + err := clip.CopyString(true, decSlice[choice]) + if err != nil { + other.PrintError("Failed to copy field to clipboard: "+err.Error(), global.ErrorClipboard) + } + } + } + // TODO clear clipboard on exit! +} diff --git a/src/cli/printInfo.go b/src/cli/printInfo.go index 346dc07..d021ca0 100644 --- a/src/cli/printInfo.go +++ b/src/cli/printInfo.go @@ -40,6 +40,7 @@ This program comes with absolutely no warranty; type "mutn version" for details. totp|-t Generate and copy the TOTP token for an entry to your clipboard url|-l Copy the URL in an entry to your clipboard note|-n Copy the first note line in an entry to your clipboard + menu|-m Open interactive menu to copy any combination of fields edit: password|-pw| Change the password in an entry username|-u Change the username in an entry