diff --git a/CHANGELOG.md b/CHANGELOG.md index 517e3ac..2197225 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog - Paw +## 0.16.0 - 28 February 2022 + +- all: fix regression about setting item date +- cli: add the "-c, --clip" option to copy password to clipboard +- cli: update messages to printed correctly on stdout and stderr +- cli:list command will show an hint message if no vaults are found +- cli,deps: add golang.design/x/clipboard +- gui,deps: update fyne.io/fyne to v2.1.3 + ## 0.15.0 - 26 January 2022 - cli: add CLI application #3 diff --git a/cmd/paw-cli/main.go b/cmd/paw-cli/main.go index 50c3c94..4582334 100644 --- a/cmd/paw-cli/main.go +++ b/cmd/paw-cli/main.go @@ -1,7 +1,7 @@ package main import ( - "log" + "fmt" "os" "lucor.dev/paw/internal/cli" @@ -12,12 +12,10 @@ import ( var Version string func main() { - - log.SetFlags(0) - s, err := paw.NewOSStorage() if err != nil { - log.Fatal(err) + fmt.Fprintf(os.Stderr, "[✗] %s\n", err) + os.Exit(1) } // Define the command to use @@ -58,12 +56,14 @@ func main() { // and will exit in case of error err = cmd.Parse(os.Args[2:]) if err != nil { - log.Fatalf("[✗] %s", err) + fmt.Fprintf(os.Stderr, "[✗] %s\n", err) + os.Exit(1) } // Finally run the command err = cmd.Run(s) if err != nil { - log.Fatalf("[✗] %s", err) + fmt.Fprintf(os.Stderr, "[✗] %s\n", err) + os.Exit(1) } } diff --git a/go.mod b/go.mod index 8676ee2..802c349 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,9 @@ go 1.16 require ( filippo.io/age v1.0.0 - fyne.io/fyne/v2 v2.1.2 + fyne.io/fyne/v2 v2.1.3 github.com/stretchr/testify v1.7.0 + golang.design/x/clipboard v0.6.0 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 golang.org/x/image v0.0.0-20211028202545-6944b10bf410 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c diff --git a/go.sum b/go.sum index ba469a6..dbdde74 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,11 @@ filippo.io/age v1.0.0 h1:V6q14n0mqYU3qKFkZ6oOaF9oXneOviS3ubXsSVBRSzc= filippo.io/age v1.0.0/go.mod h1:PaX+Si/Sd5G8LgfCwldsSba3H1DDQZhIhFGkhbHaBq8= filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= -fyne.io/fyne/v2 v2.1.2 h1:avp9CvLAUdvE7fDMtH1tVKyjxEWHWcpow6aI6L7Kvvw= -fyne.io/fyne/v2 v2.1.2/go.mod h1:p+E/Dh+wPW8JwR2DVcsZ9iXgR9ZKde80+Y+40Is54AQ= +fyne.io/fyne/v2 v2.1.3 h1:I5qSeENAcq67hmO5Z2hI7sEJm9bdLMDJx59Fv8qJkX0= +fyne.io/fyne/v2 v2.1.3/go.mod h1:p+E/Dh+wPW8JwR2DVcsZ9iXgR9ZKde80+Y+40Is54AQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= @@ -54,14 +55,27 @@ github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/X github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.8 h1:Nw158Q8QN+CPgTmVRByhVwapp8Mm1e2blinhmx4wx5E= github.com/yuin/goldmark v1.3.8/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.design/x/clipboard v0.6.0 h1:+U/e2KDBdpIjkRdxO8GwlD6dKD3Jx5zlNNzQjxte4A0= +golang.design/x/clipboard v0.6.0/go.mod h1:ep0pB+/4DGJK3ayLxweWJFHhHGGv3npJJHMXAjtLTUM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554 h1:3In5TnfvnuXTF/uflgpYxSCEGP2NdYT37KsPh3VjZYU= +golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554/go.mod h1:jFTmtFYCV0MFtXBU+J5V/+5AUeVS0ON/0WkE/KSrl6E= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -89,7 +103,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/cli/cli.go b/internal/cli/cli.go index d4c8283..1f18e38 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -4,7 +4,6 @@ import ( "bufio" "fmt" "io" - "log" "os" "path/filepath" "strconv" @@ -12,6 +11,7 @@ import ( "text/template" "golang.org/x/term" + "lucor.dev/paw/internal/paw" ) @@ -47,11 +47,13 @@ func printUsage(textTemplate string, data interface{}) { func printTemplate(w io.Writer, textTemplate string, data interface{}) { tpl, err := template.New("tpl").Parse(textTemplate) if err != nil { - log.Fatalf("Could not parse the template: %s", err) + fmt.Fprintf(os.Stderr, "[✗] could not parse the template: %s\n", err) + os.Exit(1) } err = tpl.Execute(w, data) if err != nil { - log.Fatalf("Could not execute the template: %s", err) + fmt.Fprintf(os.Stderr, "[✗] could not execute the template: %s\n", err) + os.Exit(1) } } diff --git a/internal/cli/clipboard.go b/internal/cli/clipboard.go new file mode 100644 index 0000000..c992f87 --- /dev/null +++ b/internal/cli/clipboard.go @@ -0,0 +1,49 @@ +package cli + +import ( + "bytes" + "context" + "fmt" + "time" + + "golang.design/x/clipboard" +) + +const ( + clipboardWatchInterval = 10 * time.Millisecond + clipboardWriteTimeout = 1 * time.Second +) + +func writeToClipboard(ctx context.Context, data []byte) error { + last := clipboard.Read(clipboard.FmtText) + if bytes.Equal(last, data) { + // data is the same in clipboard no need to write + return nil + } + + clipboard.Write(clipboard.FmtText, data) + + ti := time.NewTicker(clipboardWatchInterval) + defer ti.Stop() + + for { + select { + case <-ctx.Done(): + return fmt.Errorf("unable to write data to clipboard: timeout reached") + case <-ti.C: + b := clipboard.Read(clipboard.FmtText) + if b == nil { + continue + } + if bytes.Equal(last, b) { + // clipboard data not changed + continue + } + if !bytes.Equal(b, data) { + // clipboard data changed but with unexpected content + return fmt.Errorf("clipboard has been overwritten by others and data is lost") + } + return nil + } + } +} diff --git a/internal/cli/cmd_add.go b/internal/cli/cmd_add.go index ba5f87c..cb2d4b2 100644 --- a/internal/cli/cmd_add.go +++ b/internal/cli/cmd_add.go @@ -2,7 +2,6 @@ package cli import ( "fmt" - "log" "os" "lucor.dev/paw/internal/paw" @@ -100,7 +99,7 @@ func (cmd *AddCmd) Run(s paw.Storage) error { if err != nil { return err } - log.Printf("[✓] item %q added", cmd.itemName) + fmt.Printf("[✓] item %q added\n", cmd.itemName) return nil } diff --git a/internal/cli/cmd_edit.go b/internal/cli/cmd_edit.go index 8e46fd3..1b4ad99 100644 --- a/internal/cli/cmd_edit.go +++ b/internal/cli/cmd_edit.go @@ -2,8 +2,8 @@ package cli import ( "fmt" - "log" "os" + "time" "lucor.dev/paw/internal/paw" ) @@ -89,6 +89,8 @@ func (cmd *EditCmd) Run(s paw.Storage) error { return fmt.Errorf("unsupported item type: %q", cmd.itemType) } + item.GetMetadata().Modified = time.Now() + err = s.StoreItem(vault, item) if err != nil { return err @@ -101,7 +103,7 @@ func (cmd *EditCmd) Run(s paw.Storage) error { if err != nil { return err } - log.Printf("[✓] item %q modified", cmd.itemName) + fmt.Printf("[✓] item %q modified\n", cmd.itemName) return nil } diff --git a/internal/cli/cmd_init.go b/internal/cli/cmd_init.go index 04524b8..7d53489 100644 --- a/internal/cli/cmd_init.go +++ b/internal/cli/cmd_init.go @@ -2,7 +2,6 @@ package cli import ( "fmt" - "log" "os" "lucor.dev/paw/internal/paw" @@ -68,6 +67,6 @@ func (cmd *InitCmd) Run(s paw.Storage) error { if err != nil { return err } - log.Printf("[✓] vault %q created", cmd.vaultName) + fmt.Printf("[✓] vault %q created\n", cmd.vaultName) return nil } diff --git a/internal/cli/cmd_list.go b/internal/cli/cmd_list.go index ef96f17..d266121 100644 --- a/internal/cli/cmd_list.go +++ b/internal/cli/cmd_list.go @@ -2,7 +2,6 @@ package cli import ( "fmt" - "log" "os" "lucor.dev/paw/internal/paw" @@ -88,7 +87,7 @@ func (cmd *ListCmd) Run(s paw.Storage) error { } if len(n.Child) == 0 { - log.Printf("vault %q is empty", cmd.vaultName) + fmt.Printf("vault %q is empty\n", cmd.vaultName) return nil } @@ -141,6 +140,9 @@ func (cmd *ListCmd) vaults(s paw.Storage) (tree.Node, error) { if err != nil { return n, err } + if len(vaults) == 0 { + return n, fmt.Errorf("no vaults found. To create one: paw-cli init VAULT") + } for _, v := range vaults { if cmd.vaultName != "" && cmd.vaultName != v { continue diff --git a/internal/cli/cmd_pwgen.go b/internal/cli/cmd_pwgen.go index bc00cc6..45549c8 100644 --- a/internal/cli/cmd_pwgen.go +++ b/internal/cli/cmd_pwgen.go @@ -2,7 +2,6 @@ package cli import ( "fmt" - "log" "os" "lucor.dev/paw/internal/paw" @@ -61,7 +60,7 @@ func (cmd *PwGenCmd) Run(s paw.Storage) error { return err } - log.Println(password.Value) + fmt.Println(password.Value) return nil } diff --git a/internal/cli/cmd_rm.go b/internal/cli/cmd_rm.go index 93215eb..74f02e7 100644 --- a/internal/cli/cmd_rm.go +++ b/internal/cli/cmd_rm.go @@ -2,7 +2,6 @@ package cli import ( "fmt" - "log" "os" "lucor.dev/paw/internal/paw" @@ -98,6 +97,6 @@ func (cmd *RemoveCmd) Run(s paw.Storage) error { return err } - log.Printf("[✓] item %q removed", cmd.itemName) + fmt.Printf("[✓] item %q removed\n", cmd.itemName) return nil } diff --git a/internal/cli/cmd_show.go b/internal/cli/cmd_show.go index f7daa9f..2aa6419 100644 --- a/internal/cli/cmd_show.go +++ b/internal/cli/cmd_show.go @@ -1,16 +1,20 @@ package cli import ( - "log" + "context" + "fmt" "os" "time" + "golang.design/x/clipboard" + "lucor.dev/paw/internal/paw" ) // Show shows an item details type ShowCmd struct { itemPath + clipboard bool } // Name returns the one word command name @@ -30,6 +34,7 @@ func (cmd *ShowCmd) Usage() { {{ . }} Options: + -c, --clip Do not print the password but instead copy to the clipboard -h, --help Displays this help and exit ` printUsage(template, cmd.Description()) @@ -42,13 +47,23 @@ func (cmd *ShowCmd) Parse(args []string) error { return err } + flagSet.BoolVar(&cmd.clipboard, "c", false, "") + flagSet.BoolVar(&cmd.clipboard, "clip", false, "") + flagSet.Parse(args) if flags.Help || len(flagSet.Args()) != 1 { cmd.Usage() os.Exit(0) } - itemPath, err := parseItemPath(args[0], itemPathOptions{fullPath: true}) + if cmd.clipboard { + err := clipboard.Init() + if err != nil { + return err + } + } + + itemPath, err := parseItemPath(flagSet.Arg(0), itemPathOptions{fullPath: true}) if err != nil { return err } @@ -78,27 +93,46 @@ func (cmd *ShowCmd) Run(s paw.Storage) error { return err } + var pclip []byte switch cmd.itemType { case paw.LoginItemType: v := item.(*paw.Login) - log.Printf("URL: %s", v.URL) - log.Printf("Username: %s", v.Username) - log.Printf("Password: %s", v.Password.Value) + fmt.Printf("URL: %s\n", v.URL) + fmt.Printf("Username: %s\n", v.Username) + if !cmd.clipboard { + fmt.Printf("Password: %s\n", v.Password.Value) + } else { + pclip = []byte(v.Password.Value) + } if v.Note != nil { - log.Printf("Note: %s", v.Note.Value) + fmt.Printf("Note: %s\n", v.Note.Value) } case paw.PasswordItemType: v := item.(*paw.Password) - log.Printf("Password: %s", v.Value) + if !cmd.clipboard { + fmt.Printf("Password: %s\n", v.Value) + } else { + pclip = []byte(v.Value) + } if v.Note != nil { - log.Printf("Note: %s", v.Note.Value) + fmt.Printf("Note: %s\n", v.Note.Value) } case paw.NoteItemType: v := item.(*paw.Note) - log.Printf("Note: %s", v.Value) + fmt.Printf("Note: %s\n", v.Value) } - log.Printf("Created: %s", item.GetMetadata().Created.Format(time.RFC1123)) - log.Printf("Modified: %s", item.GetMetadata().Modified.Format(time.RFC1123)) + fmt.Printf("Modified: %s\n", item.GetMetadata().Modified.Format(time.RFC1123)) + fmt.Printf("Created: %s\n", item.GetMetadata().Created.Format(time.RFC1123)) + + if pclip != nil { + ctx, cancel := context.WithTimeout(context.Background(), clipboardWriteTimeout) + defer cancel() + err := writeToClipboard(ctx, pclip) + if err != nil { + return nil + } + fmt.Println("[✓] password copied to clipboard") + } return nil } diff --git a/internal/paw/item_metadata.go b/internal/paw/item_metadata.go index dee908e..72e786a 100644 --- a/internal/paw/item_metadata.go +++ b/internal/paw/item_metadata.go @@ -34,6 +34,10 @@ func (m *Metadata) GetMetadata() *Metadata { return m } +func (m *Metadata) IsEmpty() bool { + return m.Name == "" +} + func (m *Metadata) String() string { return m.Name } diff --git a/internal/paw/vault.go b/internal/paw/vault.go index b15c74d..ea2bf55 100644 --- a/internal/paw/vault.go +++ b/internal/paw/vault.go @@ -100,7 +100,7 @@ func (v *Vault) Range(f func(id string, meta *Metadata) bool) { func (v *Vault) FilterItemMetadata(opts *VaultFilterOptions) []*Metadata { metadata := []*Metadata{} - nameFilter := opts.Name + nameFilter := strings.ToLower(opts.Name) for t, itemMetadataByType := range v.ItemMetadata { if opts.ItemType != 0 && (opts.ItemType&t) == 0 { @@ -108,7 +108,7 @@ func (v *Vault) FilterItemMetadata(opts *VaultFilterOptions) []*Metadata { } for _, itemMetadata := range itemMetadataByType { - if nameFilter != "" && !strings.Contains(itemMetadata.Name, nameFilter) { + if nameFilter != "" && !strings.Contains(strings.ToLower(itemMetadata.Name), nameFilter) { continue } metadata = append(metadata, itemMetadata) diff --git a/internal/ui/vault_view.go b/internal/ui/vault_view.go index 6f82544..478c82a 100644 --- a/internal/ui/vault_view.go +++ b/internal/ui/vault_view.go @@ -11,6 +11,7 @@ import ( "strings" "sync" "sync/atomic" + "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" @@ -326,8 +327,10 @@ func (vw *vaultView) editItemView(ctx context.Context, fyneItem FyneItem) fyne.C var reloadItems bool var isNew bool - if metadata.Created == metadata.Modified { + if item.GetMetadata().IsEmpty() { isNew = true + } else { + metadata.Modified = time.Now() } if isNew && vw.vault.HasItem(editItem) {