Replace entry reader with copy menu in most cases

This commit is contained in:
2025-11-23 14:49:49 -05:00
parent b26c2ce17c
commit 8112496006
6 changed files with 74 additions and 76 deletions
+7 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+1 -5
View File
@@ -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)
}