diff --git a/CHANGELOG.md b/CHANGELOG.md index cabb68f..59f1d59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ Which is based on [Keep A Changelog](http://keepachangelog.com/) ## Unreleased +### Changed + +- New interface + ## v1.2.1 - 2019-10-19 ### Fixed diff --git a/gpm/cli.go b/gpm/cli.go index 6efca31..49c25ed 100644 --- a/gpm/cli.go +++ b/gpm/cli.go @@ -1,332 +1,190 @@ -// Copyright 2019 Adrien Waksberg -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - package gpm import ( - "bufio" "fmt" - "github.com/atotto/clipboard" - "github.com/olekukonko/tablewriter" - "golang.org/x/crypto/ssh/terminal" - "io/ioutil" - "os" - "strconv" - "syscall" - "time" + "log" + + ui "github.com/gizak/termui/v3" + "github.com/gizak/termui/v3/widgets" ) -// Cli contain config and wallet to use type Cli struct { Config Config Wallet Wallet } -// printEntries show entries with tables -func (c *Cli) printEntries(entries []Entry) { - var otp string - var tables map[string]*tablewriter.Table +func (c *Cli) ErrorBox(msg string) { + p := widgets.NewParagraph() + p.Title = "Notification" + p.SetRect(10, 0, 70, 5) + p.Text = fmt.Sprintf("[ERROR: %s](fg:red) ", msg) - tables = make(map[string]*tablewriter.Table) - - for i, entry := range entries { - if entry.OTP == "" { - otp = "" - } else { - otp = "X" - } - if _, present := tables[entry.Group]; present == false { - tables[entry.Group] = tablewriter.NewWriter(os.Stdout) - tables[entry.Group].SetHeader([]string{"", "Name", "URI", "User", "OTP", "Comment"}) - tables[entry.Group].SetBorder(false) - tables[entry.Group].SetColumnColor( - tablewriter.Colors{tablewriter.Normal, tablewriter.FgYellowColor}, - tablewriter.Colors{tablewriter.Normal, tablewriter.FgWhiteColor}, - tablewriter.Colors{tablewriter.Normal, tablewriter.FgCyanColor}, - tablewriter.Colors{tablewriter.Normal, tablewriter.FgGreenColor}, - tablewriter.Colors{tablewriter.Normal, tablewriter.FgWhiteColor}, - tablewriter.Colors{tablewriter.Normal, tablewriter.FgMagentaColor}) - } - - tables[entry.Group].Append([]string{strconv.Itoa(i), entry.Name, entry.URI, entry.User, otp, entry.Comment}) - } - - for group, table := range tables { - fmt.Printf("\n%s\n\n", group) - table.Render() - fmt.Println("") - } + ui.Render(p) } -// error print a message and exit) -func (c *Cli) error(msg string) { - fmt.Printf("ERROR: %s\n", msg) - os.Exit(2) -} +func (c *Cli) InputBox(msg string, hidden bool) string { + var input, secret string -// input from the console -func (c *Cli) input(text string, defaultValue string, show bool) string { - fmt.Print(text) + p := widgets.NewParagraph() + p.SetRect(10, 10, 70, 5) + p.Text = fmt.Sprintf("%s ", msg) - if show == false { - data, _ := terminal.ReadPassword(int(syscall.Stdin)) - text := string(data) - fmt.Printf("\n") + ui.Render(p) - if text == "" { - return defaultValue - } - return text - } - - input := bufio.NewScanner(os.Stdin) - input.Scan() - if input.Text() == "" { - return defaultValue - } - return input.Text() -} - -// selectEntry with a form -func (c *Cli) selectEntry() Entry { - var index int - - entries := c.Wallet.SearchEntry(*PATTERN, *GROUP) - if len(entries) == 0 { - fmt.Println("no entry found") - os.Exit(1) - } - - c.printEntries(entries) - if len(entries) == 1 { - return entries[0] - } - - c1 := make(chan int, 1) - go func(max int) { - for true { - index, err := strconv.Atoi(c.input("Select the entry: ", "", true)) - if err == nil && index >= 0 && index+1 <= max { - c1 <- index - break + uiEvents := ui.PollEvents() + for { + e := <-uiEvents + switch e.ID { + case "q", "": + return "" + case "": + if len(input) >= 1 { + input = input[:len(input)-1] } - fmt.Println("your choice is not an integer or is out of range") + case "": + return input + case "": + input = input + " " + default: + input = input + e.ID } - }(len(entries)) - select { - case res := <-c1: - index = res - case <-time.After(30 * time.Second): - os.Exit(1) + if hidden { + secret = "" + for i := 1; i <= int(float64(len(input)) * 1.75); i++ { + secret = secret + "*" + } + p.Text = fmt.Sprintf("%s %s", msg, secret) + } else { + p.Text = fmt.Sprintf("%s %s", msg, input) + } + ui.Render(p) } - - return entries[index] } -// loadWallet get and unlock the wallet -func (c *Cli) loadWallet() { +func (c *Cli) EntryBox(entry Entry) { + p := widgets.NewParagraph() + p.Title = "Entry" + p.SetRect(25, 0, 80, 23) + p.Text = fmt.Sprintf("%s[Name:](fg:yellow) %s\n", p.Text, entry.Name) + p.Text = fmt.Sprintf("%s[URI:](fg:yellow) %s\n", p.Text, entry.URI) + p.Text = fmt.Sprintf("%s[User:](fg:yellow) %s\n", p.Text, entry.User) + if entry.OTP == "" { + p.Text = fmt.Sprintf("%s[OTP:](fg:yellow) [no](fg:red)\n", p.Text) + } else { + p.Text = fmt.Sprintf("%s[OTP:](fg:yellow) [yes](fg:green)\n", p.Text) + } + p.Text = fmt.Sprintf("%s[Comment:](fg:yellow) %v\n", p.Text, entry.Comment) + + ui.Render(p) +} + +func (c *Cli) UnlockWallet(wallet string) error { var walletName string + var err error - passphrase := c.input("Enter the passphrase to unlock the wallet: ", "", false) - - if *WALLET == "" { + ui.Clear() + if wallet == "" { walletName = c.Config.WalletDefault } else { - walletName = *WALLET + walletName = wallet } c.Wallet = Wallet{ - Name: walletName, - Path: fmt.Sprintf("%s/%s.gpm", c.Config.WalletDir, walletName), - Passphrase: passphrase, + Name: walletName, + Path: fmt.Sprintf("%s/%s.gpm", c.Config.WalletDir, walletName), } - err := c.Wallet.Load() - if err != nil { - c.error(fmt.Sprintf("%s", err)) + for i := 0; i < 3; i++ { + c.Wallet.Passphrase = c.InputBox("Enter the passphrase to unlock the wallet:\n", true) + + err = c.Wallet.Load() + if err == nil { + return nil + } + c.ErrorBox(fmt.Sprintf("%s", err)) } + + return err } -// List the entry of a wallet -func (c *Cli) listEntry() { - c.loadWallet() - entries := c.Wallet.SearchEntry(*PATTERN, *GROUP) - if len(entries) == 0 { - fmt.Println("no entry found") - os.Exit(1) - } else { - c.printEntries(entries) - } +func (c *Cli) GroupsBox() string { + return "" } -// Delete an entry of a wallet -func (c *Cli) deleteEntry() { - var entry Entry +func (c *Cli) ListEntries() { + var pattern, group string + var entries []Entry - c.loadWallet() - entry = c.selectEntry() - confirm := c.input("are you sure you want to remove this entry [y/N] ?", "N", true) + refresh := true + index := -1 - if confirm == "y" { - err := c.Wallet.DeleteEntry(entry.ID) - if err != nil { - c.error(fmt.Sprintf("%s", err)) + l := widgets.NewList() + l.Title = "Entries" + l.TextStyle = ui.NewStyle(ui.ColorYellow) + l.SelectedRowStyle = ui.NewStyle(ui.ColorGreen, ui.ColorClear, ui.ModifierBold) + l.WrapText = false + l.SetRect(0, 0, 25, 23) + + ui.Clear() + uiEvents := ui.PollEvents() + for { + if refresh { + refresh = false + index = -1 + entries = c.Wallet.SearchEntry(pattern, group) + l.Rows = []string{} + for _, entry := range entries { + l.Rows = append(l.Rows, entry.Name) + } + ui.Clear() } - err = c.Wallet.Save() - if err != nil { - c.error(fmt.Sprintf("%s", err)) + if index >= 0 { + c.EntryBox(entries[index]) } - fmt.Println("the entry has been deleted") - } -} - -// Add a new entry in wallet -func (c *Cli) addEntry() { - c.loadWallet() - - entry := Entry{} - entry.GenerateID() - entry.Name = c.input("Enter the name: ", "", true) - entry.Group = c.input("Enter the group: ", "", true) - entry.URI = c.input("Enter the URI: ", "", true) - entry.User = c.input("Enter the username: ", "", true) - if *RANDOM { - entry.Password = RandomString(c.Config.PasswordLength, - c.Config.PasswordLetter, c.Config.PasswordDigit, c.Config.PasswordSpecial) - } else { - entry.Password = c.input("Enter the new password: ", entry.Password, false) - } - entry.OTP = c.input("Enter the OTP key: ", "", false) - entry.Comment = c.input("Enter a comment: ", "", true) - - err := c.Wallet.AddEntry(entry) - if err != nil { - c.error(fmt.Sprintf("%s", err)) - } - - err = c.Wallet.Save() - if err != nil { - c.error(fmt.Sprintf("%s", err)) - } - - fmt.Println("the entry has been added") -} - -// Update an entry in wallet -func (c *Cli) updateEntry() { - c.loadWallet() - - entry := c.selectEntry() - entry.Name = c.input("Enter the new name: ", entry.Name, true) - entry.Group = c.input("Enter the new group: ", entry.Group, true) - entry.URI = c.input("Enter the new URI: ", entry.URI, true) - entry.User = c.input("Enter the new username: ", entry.User, true) - if *RANDOM { - entry.Password = RandomString(c.Config.PasswordLength, - c.Config.PasswordLetter, c.Config.PasswordDigit, c.Config.PasswordSpecial) - } else { - entry.Password = c.input("Enter the new password: ", entry.Password, false) - } - entry.OTP = c.input("Enter the new OTP key: ", entry.OTP, false) - entry.Comment = c.input("Enter a new comment: ", entry.Comment, true) - - err := c.Wallet.UpdateEntry(entry) - if err != nil { - c.error(fmt.Sprintf("%s", err)) - } - c.Wallet.Save() -} - -// Copy login and password from an entry -func (c *Cli) copyEntry() { - c.loadWallet() - entry := c.selectEntry() - - go func() { - for true { - choice := c.input("select one action: ", "", true) - switch choice { - case "l": - clipboard.WriteAll(entry.User) - case "p": - clipboard.WriteAll(entry.Password) - case "o": - code, time, _ := entry.OTPCode() - fmt.Printf("this OTP code is available for %d seconds\n", time) - clipboard.WriteAll(code) - case "q": - clipboard.WriteAll("") - os.Exit(0) - default: - fmt.Println("l -> copy login") - fmt.Println("p -> copy password") - fmt.Println("o -> copy OTP code") - fmt.Println("q -> quit") + ui.Render(l) + e := <-uiEvents + switch e.ID { + case "q", "": + return + case "": + index = l.SelectedRow + case "": + pattern = "" + refresh = true + case "/": + pattern = c.InputBox("Search", false) + refresh = true + case "g": + group = c.GroupsBox() + refresh = true + case "j", "": + if len(entries) > 0 { + l.ScrollDown() + } + case "k", "": + if len(entries) > 0 { + l.ScrollUp() } } - }() - - select { - case <-time.After(90 * time.Second): - clipboard.WriteAll("") - os.Exit(1) } } -// Import entries from json file -func (c *Cli) ImportWallet() { - c.loadWallet() +func Run() { + var c Cli + c.Config.Load("") - _, err := os.Stat(*IMPORT) + if err := ui.Init(); err != nil { + log.Fatalf("failed to initialize termui: %v", err) + } + defer ui.Close() + + err := c.UnlockWallet("test") if err != nil { - c.error(fmt.Sprintf("%s", err)) + return } - data, err := ioutil.ReadFile(*IMPORT) - if err != nil { - c.error(fmt.Sprintf("%s", err)) - } - - err = c.Wallet.Import(data) - if err != nil { - c.error(fmt.Sprintf("%s", err)) - } - - err = c.Wallet.Save() - if err != nil { - c.error(fmt.Sprintf("%s", err)) - } - - fmt.Println("the import was successful") -} - -// Export a wallet in json format -func (c *Cli) ExportWallet() { - c.loadWallet() - - data, err := c.Wallet.Export() - if err != nil { - c.error(fmt.Sprintf("%s", err)) - } - - err = ioutil.WriteFile(*EXPORT, data, 0600) - if err != nil { - c.error(fmt.Sprintf("%s", err)) - } - - fmt.Println("the export was successful") + c.ListEntries() } diff --git a/gpm/main.go b/gpm/main.go deleted file mode 100644 index 7c09741..0000000 --- a/gpm/main.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2019 Adrien Waksberg -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package gpm - -import ( - "flag" - "fmt" - "os" -) - -// Options -var ( - ADD = flag.Bool("add", false, "add a new entry in the wallet") - UPDATE = flag.Bool("update", false, "update an entry") - DELETE = flag.Bool("delete", false, "delete an entry") - LIST = flag.Bool("list", false, "list the entries in a wallet") - LENGTH = flag.Int("length", 16, "specify the password length") - COPY = flag.Bool("copy", false, "enter an copy mode for an entry") - CONFIG = flag.String("config", "", "specify the config file") - GROUP = flag.String("group", "", "search the entries in this group ") - WALLET = flag.String("wallet", "", "specify the wallet") - PATTERN = flag.String("pattern", "", "search the entries with this pattern") - RANDOM = flag.Bool("random", false, "generate a random password for a new entry or an update") - PASSWD = flag.Bool("password", false, "generate and print a random password") - DIGIT = flag.Bool("digit", false, "use digit to generate a random password") - LETTER = flag.Bool("letter", false, "use letter to generate a random password") - SPECIAL = flag.Bool("special", false, "use special chars to generate a random password") - EXPORT = flag.String("export", "", "json file path to export a wallet") - IMPORT = flag.String("import", "", "json file path to import entries") - HELP = flag.Bool("help", false, "print this help message") -) - -// Run the cli interface -func Run() { - var cli Cli - cli.Config.Load(*CONFIG) - - flag.Parse() - if *HELP { - flag.PrintDefaults() - os.Exit(1) - } else if *PASSWD { - fmt.Println(RandomString(*LENGTH, *LETTER, *DIGIT, *SPECIAL)) - } else if *LIST { - cli.listEntry() - } else if *COPY { - cli.copyEntry() - } else if *ADD { - cli.addEntry() - } else if *UPDATE { - cli.updateEntry() - } else if *DELETE { - cli.deleteEntry() - } else if *IMPORT != "" { - cli.ImportWallet() - } else if *EXPORT != "" { - cli.ExportWallet() - } -}