mirror of
https://github.com/rwinkhart/MUTN.git
synced 2026-08-27 20:36:29 -04:00
Replace entry reader with copy menu in most cases
This commit is contained in:
@@ -54,11 +54,11 @@ func main() {
|
||||
other.PrintError("Failed to copy password to clipboard: "+err.Error(), global.ErrorClipboard)
|
||||
}
|
||||
case "edit":
|
||||
cli.EditEntryField(realPath, true, 0)
|
||||
cli.EditEntryField(realPath, 0)
|
||||
case "gen":
|
||||
cli.AddEntry(realPath, true, 1)
|
||||
cli.AddEntry(realPath, 1)
|
||||
case "add":
|
||||
cli.AddEntry(realPath, true, 0)
|
||||
cli.AddEntry(realPath, 0)
|
||||
case "shear":
|
||||
err := syncclient.ShearRemoteFromClient(args[1], false) // pass the incomplete path as the server and all clients (reading from the deletions directory) will have a different home directory
|
||||
if err != nil {
|
||||
@@ -70,11 +70,11 @@ func main() {
|
||||
} else { // handle "show" or "-s" argument for gen, edit, and add
|
||||
switch args[2] {
|
||||
case "edit":
|
||||
cli.EditEntryField(realPath, false, 0)
|
||||
cli.EditEntryField(realPath, 0)
|
||||
case "gen":
|
||||
cli.AddEntry(realPath, false, 1)
|
||||
cli.AddEntry(realPath, 1)
|
||||
case "add":
|
||||
cli.AddEntry(realPath, false, 0)
|
||||
cli.AddEntry(realPath, 0)
|
||||
default:
|
||||
cli.HelpMain()
|
||||
}
|
||||
@@ -97,7 +97,7 @@ func main() {
|
||||
case "note", "-n":
|
||||
field = 4
|
||||
case "menu", "-m":
|
||||
cli.CopyMenu(realPath, nil, "")
|
||||
cli.CopyMenu(args[1], nil, "")
|
||||
default:
|
||||
cli.HelpCopy()
|
||||
}
|
||||
@@ -128,29 +128,29 @@ func main() {
|
||||
cli.HelpEdit()
|
||||
}
|
||||
if argsCount == 4 {
|
||||
cli.EditEntryField(realPath, true, field)
|
||||
cli.EditEntryField(realPath, field)
|
||||
} else {
|
||||
switch args[4] {
|
||||
case "show", "-s":
|
||||
cli.EditEntryField(realPath, false, field)
|
||||
cli.EditEntryField(realPath, field)
|
||||
default:
|
||||
cli.EditEntryField(realPath, true, field)
|
||||
cli.EditEntryField(realPath, field)
|
||||
}
|
||||
}
|
||||
case "gen":
|
||||
if argsCount == 4 {
|
||||
switch args[3] {
|
||||
case "update", "-u":
|
||||
cli.GenUpdate(realPath, true)
|
||||
cli.GenUpdate(realPath)
|
||||
default:
|
||||
cli.HelpGen()
|
||||
}
|
||||
} else if args[3] == "update" || args[3] == "-u" {
|
||||
switch args[4] {
|
||||
case "show", "-s":
|
||||
cli.GenUpdate(realPath, false)
|
||||
cli.GenUpdate(realPath)
|
||||
default:
|
||||
cli.GenUpdate(realPath, true)
|
||||
cli.GenUpdate(realPath)
|
||||
}
|
||||
}
|
||||
cli.HelpGen()
|
||||
@@ -158,17 +158,17 @@ func main() {
|
||||
switch args[3] {
|
||||
case "password", "-pw":
|
||||
if argsCount == 4 {
|
||||
cli.AddEntry(realPath, true, 0)
|
||||
cli.AddEntry(realPath, 0)
|
||||
} else {
|
||||
switch args[4] {
|
||||
case "show", "-s":
|
||||
cli.AddEntry(realPath, false, 0)
|
||||
cli.AddEntry(realPath, 0)
|
||||
default:
|
||||
cli.AddEntry(realPath, true, 0)
|
||||
cli.AddEntry(realPath, 0)
|
||||
}
|
||||
}
|
||||
case "note", "-n":
|
||||
cli.AddEntry(realPath, true, 2)
|
||||
cli.AddEntry(realPath, 2)
|
||||
case "folder", "-f":
|
||||
err := syncclient.AddFolderRemoteFromClient(args[1]) // pass the incomplete path as the server will have a different home directory
|
||||
if err != nil {
|
||||
|
||||
+7
-7
@@ -9,14 +9,14 @@ import (
|
||||
|
||||
// AddEntry creates a new entry at realPath by taking user input via CLI prompts.
|
||||
// Requires: entryType (0 = standard password entry, 1 = auto-generated password entry, 2 = note-only entry).
|
||||
func AddEntry(realPath string, hideSecrets bool, entryType uint8) {
|
||||
func AddEntry(realPath string, entryType uint8) {
|
||||
// ensure realPath is valid
|
||||
_, err := core.EntryAddPrecheck(realPath)
|
||||
if err != nil {
|
||||
other.PrintError("Failed to add entry: "+err.Error(), back.ErrorWrite)
|
||||
}
|
||||
|
||||
var decryptedEntry []string
|
||||
var decSlice []string
|
||||
|
||||
var password string
|
||||
if entryType < 2 {
|
||||
@@ -33,20 +33,20 @@ func AddEntry(realPath string, hideSecrets bool, entryType uint8) {
|
||||
url := front.Input("URL:")
|
||||
if front.InputBinary("Add a note to this entry?") {
|
||||
note, _ := editNote([]string{})
|
||||
decryptedEntry = append([]string{password, username, totp, url}, note...)
|
||||
decSlice = append([]string{password, username, totp, url}, note...)
|
||||
} else {
|
||||
decryptedEntry = []string{password, username, totp, url}
|
||||
decSlice = []string{password, username, totp, url}
|
||||
}
|
||||
} else {
|
||||
note, _ := editNote([]string{})
|
||||
decryptedEntry = append([]string{"", "", "", ""}, note...)
|
||||
decSlice = append([]string{"", "", "", ""}, note...)
|
||||
}
|
||||
|
||||
// write and preview the new entry
|
||||
if password != "" {
|
||||
writeEntryCLI(realPath, decryptedEntry, hideSecrets, true, "")
|
||||
writeEntryCLI(realPath, decSlice, true, "")
|
||||
} else {
|
||||
writeEntryCLI(realPath, decryptedEntry, hideSecrets, false, "")
|
||||
writeEntryCLI(realPath, decSlice, false, "")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+19
-2
@@ -12,12 +12,14 @@ import (
|
||||
"github.com/rwinkhart/libmutton/clip"
|
||||
"github.com/rwinkhart/libmutton/crypt"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
"github.com/rwinkhart/libmutton/syncclient"
|
||||
)
|
||||
|
||||
// CopyMenu decrypts an entry and allows the user to
|
||||
// interactively copy fields without having to re-decrypt each time.
|
||||
// Only one of realPath or decSlice should be provided.
|
||||
func CopyMenu(realPath string, decSlice []string, oldPassword string) {
|
||||
// decSlice can be left nil to decrypt the entry specified by vanityPath.
|
||||
func CopyMenu(vanityPath string, decSlice []string, oldPassword string) {
|
||||
realPath := global.GetRealPath(vanityPath)
|
||||
var err error
|
||||
if decSlice == nil {
|
||||
// decrypt entry
|
||||
@@ -25,21 +27,36 @@ func CopyMenu(realPath string, decSlice []string, oldPassword string) {
|
||||
if err != nil {
|
||||
other.PrintError("Failed to decrypt entry: "+err.Error(), global.ErrorDecryption)
|
||||
}
|
||||
} else {
|
||||
fmt.Print("\n\n")
|
||||
_, err := syncclient.RunJob(false)
|
||||
if err != nil {
|
||||
other.PrintError("Failed to sync on copy menu entry: "+err.Error(), global.ErrorSyncProcess)
|
||||
}
|
||||
}
|
||||
|
||||
// determine populated fields in entry
|
||||
var fieldStrings = []string{"Username", "Password", "TOTP Code", "URL", "Note (first line)"}
|
||||
var indices = []int{1, 0, 2, 3, 4}
|
||||
var fields []string
|
||||
var mainFieldPopulated bool
|
||||
for i := range indices {
|
||||
if len(decSlice) > indices[i] && decSlice[indices[i]] != "" {
|
||||
fields = append(fields, fieldStrings[i])
|
||||
mainFieldPopulated = true
|
||||
if indices[i] == 0 && oldPassword != "" {
|
||||
fields = append(fields, "Old Password")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if no non-first-line notes are populated, render notes and exit
|
||||
if !mainFieldPopulated {
|
||||
EntryReader(vanityPath, decSlice, true)
|
||||
fmt.Println("Entry has no copyable fields, exiting...")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// set up signal handling for ctrl+c to clear clipboard
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
+17
-17
@@ -27,9 +27,9 @@ func RenameCli(oldVanityPath string) {
|
||||
}
|
||||
|
||||
// EditEntryField edits a field of an entry at realPath (user input).
|
||||
func EditEntryField(realPath string, hideSecrets bool, field int) {
|
||||
func EditEntryField(realPath string, field int) {
|
||||
// fetch old entry data (with all required lines present)
|
||||
decryptedEntry, err := core.GetOldEntryData(realPath, field)
|
||||
decSlice, err := core.GetOldEntryData(realPath, field)
|
||||
if err != nil {
|
||||
other.PrintError("Failed to fetch entry data: "+err.Error(), back.ErrorRead)
|
||||
}
|
||||
@@ -38,49 +38,49 @@ func EditEntryField(realPath string, hideSecrets bool, field int) {
|
||||
var oldPassword string
|
||||
switch field {
|
||||
case 0:
|
||||
oldPassword = decryptedEntry[field]
|
||||
decryptedEntry[field] = string(front.InputHidden("Password:"))
|
||||
oldPassword = decSlice[field]
|
||||
decSlice[field] = string(front.InputHidden("Password:"))
|
||||
case 1:
|
||||
decryptedEntry[field] = front.Input("Username:")
|
||||
decSlice[field] = front.Input("Username:")
|
||||
case 2:
|
||||
decryptedEntry[field] = string(front.InputHidden("TOTP secret:"))
|
||||
decSlice[field] = string(front.InputHidden("TOTP secret:"))
|
||||
case 3:
|
||||
decryptedEntry[field] = front.Input("URL:")
|
||||
decSlice[field] = front.Input("URL:")
|
||||
case 4: // edit notes fields
|
||||
// store note and non-note data separately
|
||||
fieldsMain := decryptedEntry[:4]
|
||||
fieldsNote := decryptedEntry[4:]
|
||||
fieldsMain := decSlice[:4]
|
||||
fieldsNote := decSlice[4:]
|
||||
|
||||
// edit the note
|
||||
editedNote, noteEdited := editNote(fieldsNote)
|
||||
if !noteEdited { // exit early if the note was not edited
|
||||
other.PrintError("Entry is unchanged", 0)
|
||||
}
|
||||
decryptedEntry = append(fieldsMain, editedNote...)
|
||||
decSlice = append(fieldsMain, editedNote...)
|
||||
}
|
||||
|
||||
// write and preview the modified entry
|
||||
if field == 0 {
|
||||
writeEntryCLI(realPath, decryptedEntry, hideSecrets, true, oldPassword)
|
||||
writeEntryCLI(realPath, decSlice, true, oldPassword)
|
||||
} else {
|
||||
writeEntryCLI(realPath, decryptedEntry, hideSecrets, false, oldPassword)
|
||||
writeEntryCLI(realPath, decSlice, false, oldPassword)
|
||||
}
|
||||
}
|
||||
|
||||
// GenUpdate generates a new password for an entry at realPath (user input).
|
||||
func GenUpdate(realPath string, hideSecrets bool) {
|
||||
func GenUpdate(realPath string) {
|
||||
// fetch old entry data
|
||||
decryptedEntry, err := core.GetOldEntryData(realPath, 0)
|
||||
decSlice, err := core.GetOldEntryData(realPath, 0)
|
||||
if err != nil {
|
||||
other.PrintError("Failed to fetch entry data: "+err.Error(), back.ErrorRead)
|
||||
}
|
||||
|
||||
// generate a new password
|
||||
oldPassword := decryptedEntry[0]
|
||||
decryptedEntry[0] = inputPasswordGen()
|
||||
oldPassword := decSlice[0]
|
||||
decSlice[0] = inputPasswordGen()
|
||||
|
||||
// write and preview the modified entry
|
||||
writeEntryCLI(realPath, decryptedEntry, hideSecrets, true, oldPassword)
|
||||
writeEntryCLI(realPath, decSlice, true, oldPassword)
|
||||
}
|
||||
|
||||
// editNote uses the user-specified text editor to edit an existing note (or create a new one if baseNote is empty).
|
||||
|
||||
+13
-28
@@ -9,59 +9,52 @@ import (
|
||||
"github.com/rwinkhart/go-boilerplate/other"
|
||||
"github.com/rwinkhart/libmutton/crypt"
|
||||
"github.com/rwinkhart/libmutton/global"
|
||||
"github.com/rwinkhart/libmutton/syncclient"
|
||||
)
|
||||
|
||||
const ansiShownPassword = "\033[38;5;10m"
|
||||
|
||||
// EntryReader prints the decrypted contents of a libmutton entry in a human-readable format.
|
||||
func EntryReader(decryptedEntry []string, hideSecrets, syncEnabled bool) {
|
||||
fmt.Println()
|
||||
func EntryReader(vanityPath string, decSlice []string, hideSecrets bool) {
|
||||
fmt.Print("\n" + back.AnsiBold + vanityPath + back.AnsiReset + "\n\n")
|
||||
|
||||
fieldLoop:
|
||||
for field := range decryptedEntry {
|
||||
for field := range decSlice {
|
||||
switch field {
|
||||
case 0:
|
||||
// if the first field (password) is not empty, print it
|
||||
if decryptedEntry[0] != "" {
|
||||
if decSlice[0] != "" {
|
||||
if !hideSecrets {
|
||||
fmt.Print(ansiDirectoryHeader + "Password:" + back.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[0] + back.AnsiReset + "\n\n")
|
||||
fmt.Print(ansiDirectoryHeader + "Password:" + back.AnsiReset + "\n" + ansiShownPassword + decSlice[0] + back.AnsiReset + "\n\n")
|
||||
} else {
|
||||
fmt.Print(ansiDirectoryHeader + "Password:" + back.AnsiReset + "\n" + back.AnsiWarning + "End command in \"show\" or \"-s\" to view" + back.AnsiReset + "\n\n")
|
||||
}
|
||||
}
|
||||
case 1:
|
||||
// if the second field (username) is not empty, print it
|
||||
if decryptedEntry[1] != "" {
|
||||
fmt.Print(ansiDirectoryHeader + "Username:" + back.AnsiReset + "\n" + decryptedEntry[1] + "\n\n")
|
||||
if decSlice[1] != "" {
|
||||
fmt.Print(ansiDirectoryHeader + "Username:" + back.AnsiReset + "\n" + decSlice[1] + "\n\n")
|
||||
}
|
||||
case 2:
|
||||
// if the third field (TOTP secret) is not empty, print it
|
||||
if decryptedEntry[2] != "" {
|
||||
if decSlice[2] != "" {
|
||||
if !hideSecrets {
|
||||
fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + back.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[2] + back.AnsiReset + "\n\n")
|
||||
fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + back.AnsiReset + "\n" + ansiShownPassword + decSlice[2] + back.AnsiReset + "\n\n")
|
||||
} else {
|
||||
fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + back.AnsiReset + "\n" + back.AnsiWarning + "End command in \"show\" or \"-s\" to view" + back.AnsiReset + "\n\n")
|
||||
}
|
||||
}
|
||||
case 3:
|
||||
// if the fourth field (url) is not empty, print it
|
||||
if decryptedEntry[3] != "" {
|
||||
fmt.Print(ansiDirectoryHeader + "URL:" + back.AnsiReset + "\n" + decryptedEntry[3] + "\n\n")
|
||||
if decSlice[3] != "" {
|
||||
fmt.Print(ansiDirectoryHeader + "URL:" + back.AnsiReset + "\n" + decSlice[3] + "\n\n")
|
||||
}
|
||||
case 4:
|
||||
// print the notes header
|
||||
fmt.Println(ansiDirectoryHeader + "Notes:" + back.AnsiReset)
|
||||
|
||||
// combine remaining fields into a single string (to support Markdown rendering)
|
||||
var noteLines []string
|
||||
for ; field < len(decryptedEntry); field++ {
|
||||
noteLines = append(noteLines, decryptedEntry[field])
|
||||
}
|
||||
|
||||
// render notes as Markdown
|
||||
r, _ := glamour.NewTermRenderer(glamour.WithStylesFromJSONBytes(glamourStyle()), glamour.WithPreservedNewLines(), glamour.WithWordWrap(width))
|
||||
markdownNotesString, _ := r.Render(strings.Join(noteLines, "\n"))
|
||||
markdownNotesString, _ := r.Render(strings.Join(decSlice[4:], "\n"))
|
||||
|
||||
// print the rendered Markdown notes
|
||||
fmt.Print(markdownNotesString)
|
||||
@@ -70,13 +63,6 @@ fieldLoop:
|
||||
break fieldLoop
|
||||
}
|
||||
}
|
||||
|
||||
if syncEnabled {
|
||||
_, err := syncclient.RunJob(false)
|
||||
if err != nil {
|
||||
other.PrintError("Failed to sync entries: "+err.Error(), global.ErrorSyncProcess)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EntryReaderDecrypt is a wrapper for EntryReader that first decrypts an RCW-wrapped file before sending it to EntryReader.
|
||||
@@ -89,6 +75,5 @@ func EntryReaderDecrypt(realPath string, hideSecrets bool) {
|
||||
if err != nil {
|
||||
other.PrintError("Failed to decrypt entry: "+err.Error(), global.ErrorDecryption)
|
||||
}
|
||||
EntryReader(decSlice, hideSecrets, false) // never sync if decrypting straight to EntryReader, as this means the entry could not have been modified
|
||||
// do not exit, as this is the job of EntryReader
|
||||
EntryReader(global.GetVanityPath(realPath), decSlice, hideSecrets)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/rwinkhart/go-boilerplate/back"
|
||||
"github.com/rwinkhart/go-boilerplate/front"
|
||||
@@ -29,17 +28,14 @@ func inputPasswordGen() string {
|
||||
}
|
||||
|
||||
// writeEntryCLI writes an entry to realPath and previews it (errors if no data is supplied).
|
||||
func writeEntryCLI(realPath string, decSlice []string, hideSecrets bool, passwordIsNew bool, oldPassword string) {
|
||||
func writeEntryCLI(realPath string, decSlice []string, passwordIsNew bool, oldPassword string) {
|
||||
if core.EntryIsNotEmpty(decSlice) {
|
||||
err := core.WriteEntry(realPath, decSlice, passwordIsNew)
|
||||
if err != nil {
|
||||
other.PrintError("Failed to write entry: "+err.Error(), back.ErrorWrite)
|
||||
}
|
||||
// preview the entry
|
||||
fmt.Println(back.AnsiBold + "\nEntry Preview:" + back.AnsiReset)
|
||||
EntryReader(decSlice, hideSecrets, true)
|
||||
CopyMenu("", decSlice, oldPassword)
|
||||
os.Exit(0)
|
||||
} else {
|
||||
other.PrintError("No data supplied for entry", back.ErrorTargetNotFound)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user