feat: ui improvements
ci/woodpecker/push/test Pipeline was successful Details

This commit is contained in:
Dawid Wysokiński 2024-04-25 07:21:28 +02:00
parent 1f010a778b
commit a6619bf688
Signed by: Kichiyaki
GPG Key ID: B5445E357FB8B892
1 changed files with 67 additions and 73 deletions

View File

@ -3,11 +3,11 @@ package internal
import ( import (
"errors" "errors"
"fmt" "fmt"
"slices"
"strings" "strings"
"time" "time"
"github.com/atotto/clipboard" "github.com/atotto/clipboard"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/textinput"
@ -23,19 +23,15 @@ const (
) )
type UI struct { type UI struct {
view view view view
vault Vault vault Vault
db DB db DB
list list.Model list list.Model
passwordInput textinput.Model passwordViewHelp help.Model
passwordError error passwordInput textinput.Model
passwordError error
} }
var keyBindingCopy = key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "copy"),
)
func NewUI(appName string, vault Vault) UI { func NewUI(appName string, vault Vault) UI {
passwordInput := textinput.New() passwordInput := textinput.New()
passwordInput.Focus() passwordInput.Focus()
@ -60,11 +56,14 @@ func NewUI(appName string, vault Vault) UI {
} }
} }
l.KeyMap.Quit = keyBindingQuit
return UI{ return UI{
view: viewPassword, view: viewPassword,
vault: vault, vault: vault,
list: l, list: l,
passwordInput: passwordInput, passwordInput: passwordInput,
passwordViewHelp: help.New(),
} }
} }
@ -78,59 +77,33 @@ type refreshListMsg struct {
t time.Time t time.Time
} }
var (
keyBindingCopy = key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "copy to clipboard"),
key.WithDisabled(),
)
keyBindingUnlock = key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "unlock vault"),
)
keyBindingQuit = key.NewBinding(
key.WithKeys("esc", "ctrl+c"),
key.WithHelp("esc/ctrl+c", "quit"),
)
)
//nolint:gocyclo
func (m UI) Update(teaMsg tea.Msg) (tea.Model, tea.Cmd) { func (m UI) Update(teaMsg tea.Msg) (tea.Model, tea.Cmd) {
if msg, ok := teaMsg.(tea.KeyMsg); ok {
if slices.Contains([]tea.KeyType{tea.KeyCtrlC, tea.KeyEsc}, msg.Type) {
return m, tea.Quit
}
}
switch m.view {
case viewPassword:
return m.updatePasswordView(teaMsg)
case viewList:
return m.updateListView(teaMsg)
default:
return m, tea.Quit
}
}
func (m UI) updatePasswordView(teaMsg tea.Msg) (tea.Model, tea.Cmd) {
var passwordInputCmd tea.Cmd var passwordInputCmd tea.Cmd
//nolint:revive //nolint:revive
m.passwordInput, passwordInputCmd = m.passwordInput.Update(teaMsg) m.passwordInput, passwordInputCmd = m.passwordInput.Update(teaMsg)
var cmd tea.Cmd
if msg, ok := teaMsg.(tea.KeyMsg); ok {
if msg.Type == tea.KeyEnter {
var err error
//nolint:revive
m.db, err = m.vault.DecryptDB([]byte(m.passwordInput.Value()))
if err != nil {
//nolint:revive
m.passwordError = errors.New("invalid password")
m.passwordInput.Reset()
} else {
//nolint:revive
m.view = viewList
cmd = tea.Batch(
m.list.SetItems(newListItems(m.db, time.Now())),
m.tick(),
)
}
}
}
if m.passwordInput.Value() != "" && m.passwordError != nil { if m.passwordInput.Value() != "" && m.passwordError != nil {
//nolint:revive //nolint:revive
m.passwordError = nil m.passwordError = nil
} }
return m, tea.Batch(passwordInputCmd, cmd)
}
func (m UI) updateListView(teaMsg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd var cmd tea.Cmd
switch msg := teaMsg.(type) { switch msg := teaMsg.(type) {
@ -143,18 +116,37 @@ func (m UI) updateListView(teaMsg tea.Msg) (tea.Model, tea.Cmd) {
m.tick(), m.tick(),
) )
case tea.KeyMsg: case tea.KeyMsg:
if msg.Type != tea.KeyEnter || clipboard.Unsupported { switch {
break case key.Matches(msg, keyBindingQuit):
} return m, tea.Quit
case key.Matches(msg, keyBindingUnlock):
var err error
//nolint:revive
m.db, err = m.vault.DecryptDB([]byte(m.passwordInput.Value()))
if err != nil {
//nolint:revive
m.passwordError = errors.New("invalid password")
m.passwordInput.Reset()
} else {
//nolint:revive
m.view = viewList
keyBindingUnlock.SetEnabled(false)
keyBindingCopy.SetEnabled(true)
cmd = tea.Batch(
m.list.SetItems(newListItems(m.db, time.Now())),
m.tick(),
)
}
case key.Matches(msg, keyBindingCopy) && !clipboard.Unsupported:
item, ok := m.list.SelectedItem().(listItem)
if !ok {
break
}
item, ok := m.list.SelectedItem().(listItem) otp, _, err := item.entry.GenerateOTP(item.t)
if !ok { if err == nil {
break _ = clipboard.WriteAll(otp)
} }
otp, _, err := item.entry.GenerateOTP(item.t)
if err == nil {
_ = clipboard.WriteAll(otp)
} }
} }
@ -162,7 +154,7 @@ func (m UI) updateListView(teaMsg tea.Msg) (tea.Model, tea.Cmd) {
//nolint:revive //nolint:revive
m.list, listCmd = m.list.Update(teaMsg) m.list, listCmd = m.list.Update(teaMsg)
return m, tea.Batch(cmd, listCmd) return m, tea.Batch(passwordInputCmd, cmd, listCmd)
} }
const tickDuration = time.Second const tickDuration = time.Second
@ -175,7 +167,6 @@ func (m UI) tick() tea.Cmd {
var ( var (
listStyle = lipgloss.NewStyle().Margin(1, 2) listStyle = lipgloss.NewStyle().Margin(1, 2)
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Margin(1, 0)
errStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000")) errStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000"))
) )
@ -193,7 +184,10 @@ func (m UI) View() string {
b.WriteString(errStyle.Render(m.passwordError.Error())) b.WriteString(errStyle.Render(m.passwordError.Error()))
} }
b.WriteString("\n") b.WriteString("\n")
b.WriteString(helpStyle.Render("(ctrl+c to quit)")) b.WriteString(m.passwordViewHelp.ShortHelpView([]key.Binding{
keyBindingUnlock,
keyBindingQuit,
}))
return b.String() return b.String()
} }