feat: improve ui/ux
ci/woodpecker/push/test Pipeline was successful
Details
ci/woodpecker/push/test Pipeline was successful
Details
This commit is contained in:
parent
4ffe4880f7
commit
f555ca2f0b
|
@ -10,13 +10,15 @@ import (
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const aegisVaultFileName = ".aegis_vault.json"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
appFlagPath = &cli.PathFlag{
|
appFlagPath = &cli.PathFlag{
|
||||||
Name: "path",
|
Name: "path",
|
||||||
Aliases: []string{"p"},
|
Aliases: []string{"p"},
|
||||||
Usage: "path to vault file",
|
Usage: "path to vault file",
|
||||||
Required: false,
|
Required: false,
|
||||||
DefaultText: "$HOME/.aegis_vault.json",
|
DefaultText: "$HOME/" + aegisVaultFileName,
|
||||||
}
|
}
|
||||||
appFlags = []cli.Flag{appFlagPath}
|
appFlags = []cli.Flag{appFlagPath}
|
||||||
)
|
)
|
||||||
|
@ -31,6 +33,7 @@ func newApp(name, version string) *appWrapper {
|
||||||
app.Name = name
|
app.Name = name
|
||||||
app.HelpName = name
|
app.HelpName = name
|
||||||
app.Version = version
|
app.Version = version
|
||||||
|
app.Usage = "Two-Factor Authentication (2FA) App compatible with Aegis vault format"
|
||||||
app.Commands = []*cli.Command{cmdHook, cmdTUI}
|
app.Commands = []*cli.Command{cmdHook, cmdTUI}
|
||||||
app.DefaultCommand = cmdTUI.Name
|
app.DefaultCommand = cmdTUI.Name
|
||||||
app.EnableBashCompletion = true
|
app.EnableBashCompletion = true
|
||||||
|
@ -59,5 +62,5 @@ func getVaultPath(c *cli.Context) (string, error) {
|
||||||
return "", fmt.Errorf("couldn't get user home dir: %w", err)
|
return "", fmt.Errorf("couldn't get user home dir: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return path.Join(dirname, ".aegis_vault.json"), nil
|
return path.Join(dirname, aegisVaultFileName), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
var cmdHook = &cli.Command{
|
var cmdHook = &cli.Command{
|
||||||
Name: "hook",
|
Name: "hook",
|
||||||
Usage: "Print the shell function used to execute goaegis",
|
Usage: "Prints the shell function used to execute " + appName,
|
||||||
Subcommands: []*cli.Command{
|
Subcommands: []*cli.Command{
|
||||||
{
|
{
|
||||||
Name: "bash",
|
Name: "bash",
|
||||||
|
|
|
@ -28,7 +28,7 @@ var cmdTUI = &cli.Command{
|
||||||
|
|
||||||
logger.Debug("vault file read successfully", slog.String("path", path))
|
logger.Debug("vault file read successfully", slog.String("path", path))
|
||||||
|
|
||||||
if _, err := tea.NewProgram(internal.NewUI(vault)).Run(); err != nil {
|
if _, err := tea.NewProgram(internal.NewUI(c.App.Name, vault)).Run(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,17 +3,34 @@ package main
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime/debug"
|
||||||
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
const appName = "goaegis"
|
const appName = "goaegis"
|
||||||
|
|
||||||
|
const defaultVersion = "development"
|
||||||
|
|
||||||
// this flag will be set by the build flags
|
// this flag will be set by the build flags
|
||||||
var version = "development"
|
var version = defaultVersion
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := newApp(appName, version)
|
app := newApp(appName, getVersion())
|
||||||
if err := app.Run(os.Args); err != nil {
|
if err := app.Run(os.Args); err != nil {
|
||||||
app.logger.Error("app run failed", slog.Any("error", err))
|
app.logger.Error("app run failed", slog.Any("error", err))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getVersion() string {
|
||||||
|
if version != defaultVersion {
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
buildInfo, ok := debug.ReadBuildInfo()
|
||||||
|
if !ok || slices.Contains([]string{"", "(devel)"}, buildInfo.Main.Version) {
|
||||||
|
return defaultVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildInfo.Main.Version
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package internal
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/list"
|
"github.com/charmbracelet/bubbles/list"
|
||||||
|
@ -23,9 +26,10 @@ type UI struct {
|
||||||
db DB
|
db DB
|
||||||
list list.Model
|
list list.Model
|
||||||
passwordInput textinput.Model
|
passwordInput textinput.Model
|
||||||
|
passwordError error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUI(vault Vault) UI {
|
func NewUI(appName string, vault Vault) UI {
|
||||||
passwordInput := textinput.New()
|
passwordInput := textinput.New()
|
||||||
passwordInput.Focus()
|
passwordInput.Focus()
|
||||||
passwordInput.EchoMode = textinput.EchoPassword
|
passwordInput.EchoMode = textinput.EchoPassword
|
||||||
|
@ -33,7 +37,7 @@ func NewUI(vault Vault) UI {
|
||||||
passwordInput.Width = 20
|
passwordInput.Width = 20
|
||||||
|
|
||||||
l := list.New(nil, list.NewDefaultDelegate(), 0, 0)
|
l := list.New(nil, list.NewDefaultDelegate(), 0, 0)
|
||||||
l.Title = "GoAegis"
|
l.Title = appName
|
||||||
l.SetShowPagination(false)
|
l.SetShowPagination(false)
|
||||||
|
|
||||||
return UI{
|
return UI{
|
||||||
|
@ -55,23 +59,37 @@ type refreshListMsg struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
var cmd tea.Cmd
|
||||||
|
|
||||||
switch msg := teaMsg.(type) {
|
if msg, ok := teaMsg.(tea.KeyMsg); ok {
|
||||||
case tea.KeyMsg:
|
if msg.Type == tea.KeyEnter {
|
||||||
//nolint:exhaustive
|
|
||||||
switch msg.Type {
|
|
||||||
case tea.KeyCtrlC, tea.KeyEsc:
|
|
||||||
return m, tea.Quit
|
|
||||||
case tea.KeyEnter:
|
|
||||||
var err error
|
var err error
|
||||||
//nolint:revive
|
//nolint:revive
|
||||||
m.db, err = m.vault.DecryptDB([]byte(m.passwordInput.Value()))
|
m.db, err = m.vault.DecryptDB([]byte(m.passwordInput.Value()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
//nolint:revive
|
||||||
|
m.passwordError = errors.New("invalid password")
|
||||||
m.passwordInput.Reset()
|
m.passwordInput.Reset()
|
||||||
} else {
|
} else {
|
||||||
//nolint:revive
|
//nolint:revive
|
||||||
|
@ -82,8 +100,22 @@ func (m UI) Update(teaMsg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.passwordInput.Value() != "" && m.passwordError != nil {
|
||||||
|
//nolint:revive
|
||||||
|
m.passwordError = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, tea.Batch(passwordInputCmd, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m UI) updateListView(teaMsg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cmd tea.Cmd
|
||||||
|
|
||||||
|
switch msg := teaMsg.(type) {
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
h, v := docStyle.GetFrameSize()
|
h, v := listStyle.GetFrameSize()
|
||||||
m.list.SetSize(msg.Width-h, msg.Height-v)
|
m.list.SetSize(msg.Width-h, msg.Height-v)
|
||||||
case refreshListMsg:
|
case refreshListMsg:
|
||||||
cmd = tea.Batch(
|
cmd = tea.Batch(
|
||||||
|
@ -96,7 +128,7 @@ func (m UI) Update(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(passwordInputCmd, cmd, listCmd)
|
return m, tea.Batch(cmd, listCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tickDuration = time.Second
|
const tickDuration = time.Second
|
||||||
|
@ -107,18 +139,29 @@ func (m UI) tick() tea.Cmd {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var docStyle = lipgloss.NewStyle().Margin(1, 2)
|
var (
|
||||||
|
listStyle = lipgloss.NewStyle().Margin(1, 2)
|
||||||
|
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Margin(1, 0)
|
||||||
|
errStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000"))
|
||||||
|
)
|
||||||
|
|
||||||
func (m UI) View() string {
|
func (m UI) View() string {
|
||||||
if m.view == viewList {
|
if m.view == viewList {
|
||||||
return docStyle.Render(m.list.View())
|
return listStyle.Render(m.list.View())
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf(
|
var b strings.Builder
|
||||||
"Enter password:\n\n%s\n\n%s",
|
|
||||||
m.passwordInput.View(),
|
b.WriteString("Enter password:\n\n")
|
||||||
"(ctrl+c to quit)",
|
b.WriteString(m.passwordInput.View())
|
||||||
) + "\n"
|
b.WriteString("\n")
|
||||||
|
if m.passwordError != nil {
|
||||||
|
b.WriteString(errStyle.Render(m.passwordError.Error()))
|
||||||
|
}
|
||||||
|
b.WriteString("\n")
|
||||||
|
b.WriteString(helpStyle.Render("(ctrl+c to quit)"))
|
||||||
|
|
||||||
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
type listItem struct {
|
type listItem struct {
|
||||||
|
|
Loading…
Reference in New Issue