diff --git a/cmd/zrok/invite.go b/cmd/zrok/invite.go index 8a5a91d4..ca7579fb 100644 --- a/cmd/zrok/invite.go +++ b/cmd/zrok/invite.go @@ -2,13 +2,17 @@ package main import ( "fmt" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" "github.com/openziti-test-kitchen/zrok/rest_client_zrok/account" "github.com/openziti-test-kitchen/zrok/rest_model_zrok" "github.com/openziti-test-kitchen/zrok/tui" "github.com/openziti-test-kitchen/zrok/util" "github.com/openziti-test-kitchen/zrok/zrokdir" - "github.com/openziti/foundation/v2/term" "github.com/spf13/cobra" + "os" + "strings" ) func init() { @@ -17,6 +21,7 @@ func init() { type inviteCommand struct { cmd *cobra.Command + tui inviteTui } func newInviteCommand() *inviteCommand { @@ -25,50 +30,188 @@ func newInviteCommand() *inviteCommand { Short: "Invite a new user to zrok", Args: cobra.ExactArgs(0), } - command := &inviteCommand{cmd: cmd} + command := &inviteCommand{ + cmd: cmd, + tui: newInviteTui(), + } cmd.Run = command.run return command } func (cmd *inviteCommand) run(_ *cobra.Command, _ []string) { - email, err := term.Prompt("New Email: ") - if err != nil { - panic(err) - } - if !util.IsValidEmail(email) { - tui.Error(fmt.Sprintf("'%v' is not a valid email address", email), nil) - } - confirm, err := term.Prompt("Confirm Email: ") - if err != nil { - panic(err) - } - if confirm != email { - tui.Error("entered emails do not match... aborting!", nil) + if _, err := tea.NewProgram(&cmd.tui).Run(); err != nil { + tui.Error("unable to run interface", err) + os.Exit(1) } + if cmd.tui.done { + email := cmd.tui.inputs[0].Value() - zrd, err := zrokdir.Load() - if err != nil { - tui.Error("error loading zrokdir", err) - } - - zrok, err := zrd.Client() - if err != nil { - if !panicInstead { - tui.Error("error creating zrok api client", err) + zrd, err := zrokdir.Load() + if err != nil { + tui.Error("error loading zrokdir", err) } - panic(err) - } - req := account.NewInviteParams() - req.Body = &rest_model_zrok.InviteRequest{ - Email: email, - } - _, err = zrok.Account.Invite(req) - if err != nil { - if !panicInstead { - tui.Error("error creating invitation", err) - } - panic(err) - } - fmt.Printf("invitation sent to '%v'!\n", email) + zrok, err := zrd.Client() + if err != nil { + if !panicInstead { + tui.Error("error creating zrok api client", err) + } + panic(err) + } + req := account.NewInviteParams() + req.Body = &rest_model_zrok.InviteRequest{ + Email: email, + } + _, err = zrok.Account.Invite(req) + if err != nil { + if !panicInstead { + tui.Error("error creating invitation", err) + } + panic(err) + } + + fmt.Printf("invitation sent to '%v'!\n", email) + } +} + +type inviteTui struct { + focusIndex int + msg string + inputs []textinput.Model + cursorMode textinput.CursorMode + done bool + + msgOk string + msgMismatch string + focusedStyle lipgloss.Style + blurredStyle lipgloss.Style + errorStyle lipgloss.Style + cursorStyle lipgloss.Style + noStyle lipgloss.Style + helpStyle lipgloss.Style + focusedButton string + blurredButton string +} + +func newInviteTui() inviteTui { + m := inviteTui{ + inputs: make([]textinput.Model, 2), + } + m.focusedStyle = tui.WarningStyle + m.blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#555")) + m.errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#F00")) + m.cursorStyle = m.focusedStyle.Copy() + m.noStyle = lipgloss.NewStyle() + m.helpStyle = m.blurredStyle.Copy() + m.focusedButton = m.focusedStyle.Copy().Render("[ Submit ]") + m.blurredButton = fmt.Sprintf("[ %v ]", m.blurredStyle.Render("Submit")) + m.msgOk = m.noStyle.Render("Enter and confirm your email address...") + m.msg = m.msgOk + m.msgMismatch = m.errorStyle.Render("Email is invalid or does not match confirmation...") + + var t textinput.Model + for i := range m.inputs { + t = textinput.New() + t.CursorStyle = m.cursorStyle + t.CharLimit = 96 + + switch i { + case 0: + t.Placeholder = "Email Address" + t.Focus() + t.PromptStyle = m.focusedStyle + t.TextStyle = m.focusedStyle + case 1: + t.Placeholder = "Confirm Email" + } + + m.inputs[i] = t + } + + return m +} + +func (m inviteTui) Init() tea.Cmd { return textinput.Blink } + +func (m *inviteTui) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "esc": + return m, tea.Quit + + case "tab", "shift+tab", "enter", "up", "down": + s := msg.String() + + if s == "enter" && m.focusIndex == len(m.inputs) { + if util.IsValidEmail(m.inputs[0].Value()) && m.inputs[0].Value() == m.inputs[1].Value() { + m.done = true + return m, tea.Quit + } + m.msg = m.msgMismatch + return m, nil + } + + if s == "up" || s == "shift+tab" { + m.msg = m.msgOk + m.focusIndex-- + } else { + m.msg = m.msgOk + m.focusIndex++ + } + + if m.focusIndex > len(m.inputs) { + m.focusIndex = 0 + } else if m.focusIndex < 0 { + m.focusIndex = len(m.inputs) + } + + cmds := make([]tea.Cmd, len(m.inputs)) + for i := 0; i <= len(m.inputs)-1; i++ { + if i == m.focusIndex { + cmds[i] = m.inputs[i].Focus() + m.inputs[i].PromptStyle = m.focusedStyle + m.inputs[i].TextStyle = m.focusedStyle + continue + } + m.inputs[i].Blur() + m.inputs[i].PromptStyle = m.noStyle + m.inputs[i].TextStyle = m.noStyle + } + + return m, tea.Batch(cmds...) + } + } + + cmd := m.updateInputs(msg) + + return m, cmd +} + +func (m *inviteTui) updateInputs(msg tea.Msg) tea.Cmd { + cmds := make([]tea.Cmd, len(m.inputs)) + for i := range m.inputs { + m.inputs[i], cmds[i] = m.inputs[i].Update(msg) + } + return tea.Batch(cmds...) +} + +func (m inviteTui) View() string { + var b strings.Builder + b.WriteString(fmt.Sprintf("\n%v\n\n", m.msg)) + + for i := range m.inputs { + b.WriteString(m.inputs[i].View()) + if i < len(m.inputs)-1 { + b.WriteRune('\n') + } + } + + button := &m.blurredButton + if m.focusIndex == len(m.inputs) { + button = &m.focusedButton + } + _, _ = fmt.Fprintf(&b, "\n\n%s\n\n", *button) + + return b.String() } diff --git a/cmd/zrok/inviteTui.go b/cmd/zrok/inviteTui.go new file mode 100644 index 00000000..06ab7d0f --- /dev/null +++ b/cmd/zrok/inviteTui.go @@ -0,0 +1 @@ +package main diff --git a/go.mod b/go.mod index aa5166ee..c98cb934 100644 --- a/go.mod +++ b/go.mod @@ -40,8 +40,10 @@ require ( require ( github.com/Jeffail/gabs v1.4.0 // indirect github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52 v1.0.3 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/charmbracelet/bubbles v0.14.0 // indirect github.com/containerd/console v1.0.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/deepmap/oapi-codegen v1.8.2 // indirect diff --git a/go.sum b/go.sum index fae55348..163be561 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg= github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= @@ -60,8 +62,13 @@ github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqO github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/charmbracelet/bubbles v0.14.0 h1:DJfCwnARfWjZLvMglhSQzo76UZ2gucuHPy9jLWX45Og= +github.com/charmbracelet/bubbles v0.14.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc= +github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4= github.com/charmbracelet/bubbletea v0.23.1 h1:CYdteX1wCiCzKNUlwm25ZHBIc1GXlYFyUIte8WPvhck= github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY= github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -326,6 +333,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -411,12 +419,14 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0= github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= github.com/netfoundry/secretstream v0.1.2 h1:NgqrYytDnjKbOfWI29TT0SJM+RwB3yf9MIkJVJaU+J0= @@ -488,6 +498,7 @@ github.com/rubenv/sql-migrate v1.1.2/go.mod h1:/7TZymwxN8VWumcIxw1jjHEcR1djpdkMH github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shirou/gopsutil/v3 v3.22.8 h1:a4s3hXogo5mE2PfdfJIonDbstO/P+9JszdfhAHSzD9Y= github.com/shirou/gopsutil/v3 v3.22.8/go.mod h1:s648gW4IywYzUfE/KjXxUsqrqx/T2xO5VqOXxONeRfI= @@ -765,6 +776,7 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=