diff --git a/cmd/enpasscli/main.go b/cmd/enpasscli/main.go index f48882a..f5e4499 100644 --- a/cmd/enpasscli/main.go +++ b/cmd/enpasscli/main.go @@ -10,10 +10,12 @@ import ( "strconv" "strings" + "github.com/gdamore/tcell/v2" "github.com/hazcod/enpass-cli/pkg/clipboard" "github.com/hazcod/enpass-cli/pkg/enpass" "github.com/hazcod/enpass-cli/pkg/unlock" "github.com/miquella/ask" + "github.com/rivo/tview" "github.com/sirupsen/logrus" ) @@ -26,6 +28,8 @@ const ( cmdShow = "show" cmdCopy = "copy" cmdPass = "pass" + cmdUi = "ui" + // defaults defaultLogLevel = logrus.InfoLevel pinMinLength = 8 @@ -36,8 +40,10 @@ var ( // overwritten by go build version = "dev" // set of all commands - commands = map[string]struct{}{cmdVersion: {}, cmdHelp: {}, cmdDryRun: {}, cmdList: {}, - cmdShow: {}, cmdCopy: {}, cmdPass: {}} + commands = map[string]struct{}{ + cmdVersion: {}, cmdHelp: {}, cmdDryRun: {}, cmdList: {}, + cmdShow: {}, cmdCopy: {}, cmdPass: {}, cmdUi: {}, + } ) type Args struct { @@ -198,6 +204,93 @@ func entryPassword(logger *logrus.Logger, vault *enpass.Vault, args *Args) { } } +func ui(logger *logrus.Logger, vault *enpass.Vault, args *Args) { + cards, err := vault.GetEntries(*args.cardType, args.filters) + if err != nil { + logger.WithError(err).Fatal("could not retrieve cards") + } + if *args.sort { + sortEntries(cards) + } + + app := tview.NewApplication() + flex := tview.NewFlex().SetDirection(tview.FlexRow) + table := tview.NewTable().SetBorders(false) + flex.AddItem(table, 0, 1, true) + + var visibleCards []enpass.Card + render := func(filter string) { + filter = strings.ToLower(filter) + visibleCards = []enpass.Card{} + + table.Clear() + table.SetCell(0, 0, tview.NewTableCell("Title").SetBackgroundColor(tcell.ColorGray)) + table.SetCell(0, 1, tview.NewTableCell("Subtitle").SetBackgroundColor(tcell.ColorGray)) + table.SetCell(0, 2, tview.NewTableCell("Category").SetBackgroundColor(tcell.ColorGray)) + + i := 0 + for _, card := range cards { + if card.IsTrashed() && !*args.trashed { + continue + } + if !strings.Contains(strings.ToLower(card.Title+" "+card.Subtitle), filter) { + continue + } + + table.SetCell(i+1, 0, tview.NewTableCell(card.Title)) + table.SetCell(i+1, 1, tview.NewTableCell(card.Subtitle)) + table.SetCell(i+1, 2, tview.NewTableCell(card.Category)) + i += 1 + visibleCards = append(visibleCards, card) + } + } + render("") // render ininital table without filter + + statusText := tview.NewTextView().SetChangedFunc(func() { + app.Draw() + }) + + inputField := tview.NewInputField() + inputField.SetLabel("Search: "). + SetFieldWidth(30). + SetDoneFunc(func(key tcell.Key) { + render(inputField.GetText()) + app.SetFocus(table) + statusText.SetText(fmt.Sprintf("found %d", len(visibleCards))) + }) + + status := tview.NewFlex() + status.AddItem(inputField, 0, 1, false) + status.AddItem(statusText, 0, 1, false) + flex.AddItem(status, 1, 1, false) + + table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Rune() == '/' { + app.SetFocus(inputField) + } + return event + }) + + table.Select(0, 0).SetFixed(1, 1) + table.SetSelectable(true, false) + table.SetSelectedFunc(func(row int, column int) { + card := visibleCards[row-1] + if decrypted, err := card.Decrypt(); err != nil { + logger.WithError(err).Fatal("could not decrypt card") + } else { + if err := clipboard.WriteAll(decrypted); err != nil { + logger.WithError(err).Fatal("could not copy password to clipboard") + } else { + statusText.SetText("copied password for " + card.Title) + } + } + }) + + if err := app.SetRoot(flex, true).SetFocus(inputField).Run(); err != nil { + panic(err) + } +} + func assembleVaultCredentials(logger *logrus.Logger, args *Args, store *unlock.SecureStore) *enpass.VaultCredentials { credentials := &enpass.VaultCredentials{ Password: os.Getenv("MASTERPW"), @@ -313,6 +406,8 @@ func main() { copyEntry(logger, vault, args) case cmdPass: entryPassword(logger, vault, args) + case cmdUi: + ui(logger, vault, args) default: logger.WithField("command", args.command).Fatal("unknown command") } diff --git a/go.mod b/go.mod index 4aedea0..c16592a 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,22 @@ go 1.17 require ( github.com/atotto/clipboard v0.1.4 + github.com/gdamore/tcell/v2 v2.7.1 github.com/miquella/ask v1.0.0 github.com/mutecomm/go-sqlcipher v0.0.0-20190227152316-55dbde17881f github.com/pkg/errors v0.9.1 + github.com/rivo/tview v0.0.0-20240921122403-a64fc48d7654 github.com/sirupsen/logrus v1.9.3 golang.org/x/crypto v0.21.0 ) require ( + github.com/gdamore/encoding v1.0.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.4.7 // indirect golang.org/x/net v0.22.0 // indirect golang.org/x/sys v0.18.0 // indirect + golang.org/x/term v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index c287358..12838a3 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,14 @@ github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.7.1 h1:TiCcmpWHiAU7F0rA2I3S2Y4mmLmO9KHxJ7E1QhYzQbc= +github.com/gdamore/tcell/v2 v2.7.1/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/miquella/ask v1.0.0 h1:QrFtpgA7tbDSlPUUwCMaAzZLnWseFZtryAn/pnvd3d8= github.com/miquella/ask v1.0.0/go.mod h1:5hBixDZi2issKiqBf4oQ5c8BauqAYOOrkFOjG4eiUWk= github.com/mutecomm/go-sqlcipher v0.0.0-20190227152316-55dbde17881f h1:hd3r+uv9DNLScbOrnlj82rBldHQf3XWmCeXAWbw8euQ= @@ -11,6 +19,12 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/tview v0.0.0-20240921122403-a64fc48d7654 h1:oa+fljZiaJUVyiT7WgIM3OhirtwBm0LJA97LvWUlBu8= +github.com/rivo/tview v0.0.0-20240921122403-a64fc48d7654/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -53,12 +67,14 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=