github.com/GGP1/kure@v0.8.4/commands/card/edit/edit.go (about) 1 package edit 2 3 import ( 4 "bufio" 5 "encoding/json" 6 "fmt" 7 "io" 8 "os" 9 "os/exec" 10 "strings" 11 12 "github.com/GGP1/kure/auth" 13 cmdutil "github.com/GGP1/kure/commands" 14 "github.com/GGP1/kure/db/card" 15 "github.com/GGP1/kure/pb" 16 "github.com/GGP1/kure/sig" 17 "github.com/GGP1/kure/terminal" 18 19 "github.com/pkg/errors" 20 "github.com/spf13/cobra" 21 bolt "go.etcd.io/bbolt" 22 ) 23 24 const example = ` 25 * Edit using the standard input 26 kure card edit Sample 27 28 * Edit using the text editor 29 kure card edit Sample -i` 30 31 type editOptions struct { 32 interactive bool 33 } 34 35 // NewCmd returns a new command. 36 func NewCmd(db *bolt.DB) *cobra.Command { 37 opts := editOptions{} 38 cmd := &cobra.Command{ 39 Use: "edit <name>", 40 Short: "Edit a card", 41 Long: `Edit a card. 42 43 If the name is edited, Kure will remove the old card and create one with the new name.`, 44 Example: example, 45 Args: cmdutil.MustExist(db, cmdutil.Card), 46 PreRunE: auth.Login(db), 47 RunE: runEdit(db, &opts), 48 PostRun: func(cmd *cobra.Command, args []string) { 49 // Reset variables (session) 50 opts = editOptions{} 51 }, 52 } 53 54 cmd.Flags().BoolVarP(&opts.interactive, "it", "i", false, "use the text editor") 55 56 return cmd 57 } 58 59 func runEdit(db *bolt.DB, opts *editOptions) cmdutil.RunEFunc { 60 return func(cmd *cobra.Command, args []string) error { 61 name := strings.Join(args, " ") 62 name = cmdutil.NormalizeName(name) 63 64 oldCard, err := card.Get(db, name) 65 if err != nil { 66 return err 67 } 68 69 if opts.interactive { 70 return useTextEditor(db, oldCard) 71 } 72 73 return useStdin(db, os.Stdin, oldCard) 74 } 75 } 76 77 func createTempFile(c *pb.Card) (string, error) { 78 f, err := os.CreateTemp("", "*.json") 79 if err != nil { 80 return "", errors.Wrap(err, "creating temporary file") 81 } 82 83 content, err := json.MarshalIndent(c, "", " ") 84 if err != nil { 85 return "", errors.Wrap(err, "encoding card") 86 } 87 88 if _, err := f.Write(content); err != nil { 89 return "", errors.Wrap(err, "writing temporary file") 90 } 91 92 if err := f.Close(); err != nil { 93 return "", errors.Wrap(err, "closing temporary file") 94 } 95 96 return f.Name(), nil 97 } 98 99 // readTmpFile reads the modified file and formats the card. 100 func readTmpFile(filename string) (*pb.Card, error) { 101 var c pb.Card 102 103 f, err := os.Open(filename) 104 if err != nil { 105 return nil, errors.Wrap(err, "reading file") 106 } 107 defer f.Close() 108 109 if err := json.NewDecoder(f).Decode(&c); err != nil { 110 return nil, errors.Wrap(err, "decoding file") 111 } 112 113 return &c, nil 114 } 115 116 // updateCard takes the name of the card that's being edited to check if the name was 117 // changed. If it was, it will remove the old one. 118 func updateCard(db *bolt.DB, name string, c *pb.Card) error { 119 if c.Name == "" { 120 return cmdutil.ErrInvalidName 121 } 122 123 name = cmdutil.NormalizeName(name) 124 c.Name = cmdutil.NormalizeName(c.Name) 125 126 if err := card.Update(db, name, c); err != nil { 127 return err 128 } 129 130 fmt.Println(c.Name, "updated") 131 return nil 132 } 133 134 func useStdin(db *bolt.DB, r io.Reader, oldCard *pb.Card) error { 135 fmt.Println("Type '-' to clear the field or leave blank to use the current value") 136 reader := bufio.NewReader(r) 137 138 scanln := func(field, value string) string { 139 input := terminal.Scanln(reader, fmt.Sprintf("%s [%s]", field, value)) 140 if input == "-" { 141 return "" 142 } else if input != "" { 143 return input 144 } 145 return value 146 } 147 148 newCard := &pb.Card{ 149 Name: scanln("Name", oldCard.Name), 150 Type: scanln("Type", oldCard.Type), 151 Number: scanln("Number", oldCard.Number), 152 SecurityCode: scanln("Security code", oldCard.SecurityCode), 153 ExpireDate: scanln("Expire date", oldCard.ExpireDate), 154 } 155 156 notes := terminal.Scanlns(reader, fmt.Sprintf("Notes [%s]", oldCard.Notes)) 157 if notes == "" { 158 notes = oldCard.Notes 159 } else if notes == "-" { 160 notes = "" 161 } 162 newCard.Notes = notes 163 164 return updateCard(db, oldCard.Name, newCard) 165 } 166 167 func useTextEditor(db *bolt.DB, oldCard *pb.Card) error { 168 editor := cmdutil.SelectEditor() 169 bin, err := exec.LookPath(editor) 170 if err != nil { 171 return errors.Errorf("executable %q not found", editor) 172 } 173 174 filename, err := createTempFile(oldCard) 175 if err != nil { 176 return err 177 } 178 179 sig.Signal.AddCleanup(func() error { return cmdutil.Erase(filename) }) 180 defer cmdutil.Erase(filename) 181 182 // Open the temporary file with the selected text editor 183 edit := exec.Command(bin, filename) 184 edit.Stdin = os.Stdin 185 edit.Stdout = os.Stdout 186 187 if err := edit.Start(); err != nil { 188 return errors.Wrapf(err, "running %s", editor) 189 } 190 191 done := make(chan struct{}, 1) 192 errCh := make(chan error, 1) 193 go cmdutil.WatchFile(filename, done, errCh) 194 195 // Block until an event is received or an error occurs 196 select { 197 case <-done: 198 case err := <-errCh: 199 return err 200 } 201 202 if err := edit.Wait(); err != nil { 203 return err 204 } 205 206 newCard, err := readTmpFile(filename) 207 if err != nil { 208 return err 209 } 210 211 rmTabs := func(old string) string { 212 return strings.ReplaceAll(old, "\t", "") 213 } 214 newCard.Name = rmTabs(newCard.Name) 215 newCard.Type = rmTabs(newCard.Type) 216 newCard.Number = rmTabs(newCard.Number) 217 newCard.SecurityCode = rmTabs(newCard.SecurityCode) 218 newCard.ExpireDate = rmTabs(newCard.ExpireDate) 219 newCard.Notes = rmTabs(newCard.Notes) 220 221 return updateCard(db, oldCard.Name, newCard) 222 }