From e7a2f49de427b783fc0951d4661b2555e36c2031 Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:10:24 -0700 Subject: [PATCH 1/6] Create text input --- go.mod | 3 ++ go.sum | 6 ++++ internal/prompt/text-input.go | 54 +++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 internal/prompt/text-input.go diff --git a/go.mod b/go.mod index e5c826f96..4be918fb7 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/onflow/flow-cli go 1.20 require ( + github.com/charmbracelet/bubbles v0.18.0 github.com/charmbracelet/bubbletea v0.25.0 github.com/dukex/mixpanel v1.0.1 github.com/getsentry/sentry-go v0.27.0 @@ -50,6 +51,7 @@ require ( github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect github.com/StackExchange/wmi v1.2.1 // indirect github.com/VictoriaMetrics/fastcache v1.12.1 // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.10.0 // indirect @@ -59,6 +61,7 @@ require ( github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/charmbracelet/lipgloss v0.9.1 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/cloudflare/circl v1.3.3 // indirect github.com/cockroachdb/errors v1.9.1 // indirect diff --git a/go.sum b/go.sum index ff0b4ddb1..5941bb602 100644 --- a/go.sum +++ b/go.sum @@ -126,6 +126,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= @@ -179,8 +181,12 @@ github.com/cespare/xxhash/v2 v2.0.1-0.20190104013014-3767db7a7e18/go.mod h1:HD5P github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= +github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= +github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= diff --git a/internal/prompt/text-input.go b/internal/prompt/text-input.go new file mode 100644 index 000000000..fc3f23532 --- /dev/null +++ b/internal/prompt/text-input.go @@ -0,0 +1,54 @@ +package prompt + +import ( + "fmt" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" +) + +type TextInputModel struct { + textInput textinput.Model + err error + customMsg string +} + +// NewTextInput initializes a new text input model with a custom message +func NewTextInput(customMsg, placeholder string) TextInputModel { + ti := textinput.New() + ti.Placeholder = placeholder + ti.Focus() + ti.CharLimit = 256 + ti.Width = 30 + + return TextInputModel{ + textInput: ti, + customMsg: customMsg, + } +} + +func (m TextInputModel) Init() tea.Cmd { + return textinput.Blink +} + +func (m TextInputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.Type { + case tea.KeyEnter, tea.KeyCtrlC, tea.KeyEsc: + return m, tea.Quit + } + var cmd tea.Cmd + m.textInput, cmd = m.textInput.Update(msg) + return m, cmd + } + + return m, nil +} + +func (m TextInputModel) View() string { + return fmt.Sprintf("%s\n\n%s\n\n%s", m.customMsg, m.textInput.View(), "(Enter to submit, Esc to quit)") +} + +func (m TextInputModel) GetValue() string { + return m.textInput.Value() +} From 10ba978598315676cb906d40ea441d9893113fe5 Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:11:53 -0700 Subject: [PATCH 2/6] Replace existing prompt --- internal/super/setup.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/internal/super/setup.go b/internal/super/setup.go index c9f5d230f..027a066c1 100644 --- a/internal/super/setup.go +++ b/internal/super/setup.go @@ -98,7 +98,16 @@ func create( } else { // Ask for project name if not given if len(args) < 1 { - name := prompt.NamePrompt() + m := prompt.NewTextInput("Enter the name of your project", "Hello World") + finalModel, err := tea.NewProgram(m).Run() + + if err != nil { + fmt.Printf("Error running program: %v\n", err) + os.Exit(1) + } + + name := finalModel.(prompt.TextInputModel).GetValue() + targetDir, err = getTargetDirectory(name) if err != nil { return nil, err From 95ff190a2166849aa382eb0f0d3e33723091cf7a Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:26:17 -0700 Subject: [PATCH 3/6] Add Run methods to each prompt --- internal/prompt/select-options.go | 17 +++++++++++++++++ internal/prompt/text-input.go | 13 +++++++++++-- internal/super/setup.go | 28 ++++++++++------------------ 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/internal/prompt/select-options.go b/internal/prompt/select-options.go index 2d15a69ae..029249229 100644 --- a/internal/prompt/select-options.go +++ b/internal/prompt/select-options.go @@ -99,3 +99,20 @@ func (m OptionSelectModel) View() string { } return b.String() } + +// RunSelectOptions handles creating, running the select options prompt, and returning selected choices +func RunSelectOptions(options []string, message string) ([]string, error) { + model := SelectOptions(options, message) + p := tea.NewProgram(model) + finalModel, err := p.Run() + if err != nil { + return nil, err + } + + final := finalModel.(OptionSelectModel) + selectedChoices := make([]string, 0) + for i := range final.Selected { + selectedChoices = append(selectedChoices, final.Choices[i]) + } + return selectedChoices, nil +} diff --git a/internal/prompt/text-input.go b/internal/prompt/text-input.go index fc3f23532..7889ea5f5 100644 --- a/internal/prompt/text-input.go +++ b/internal/prompt/text-input.go @@ -49,6 +49,15 @@ func (m TextInputModel) View() string { return fmt.Sprintf("%s\n\n%s\n\n%s", m.customMsg, m.textInput.View(), "(Enter to submit, Esc to quit)") } -func (m TextInputModel) GetValue() string { - return m.textInput.Value() +// RunTextInput handles running the text input and retrieving the result +func RunTextInput(customMsg, placeholder string) (string, error) { + model := NewTextInput(customMsg, placeholder) + p := tea.NewProgram(model) + + if finalModel, err := p.Run(); err != nil { + return "", err // return the error to handle it outside if necessary + } else { + final := finalModel.(TextInputModel) + return final.textInput.Value(), nil // directly return the input value + } } diff --git a/internal/super/setup.go b/internal/super/setup.go index 027a066c1..cedb04fa8 100644 --- a/internal/super/setup.go +++ b/internal/super/setup.go @@ -21,14 +21,14 @@ package super import ( "bytes" "fmt" + flowsdk "github.com/onflow/flow-go-sdk" + "golang.org/x/exp/slices" "io" "os" "path/filepath" "github.com/onflow/flow-cli/internal/prompt" - tea "github.com/charmbracelet/bubbletea" - flowsdk "github.com/onflow/flow-go-sdk" "github.com/onflow/flow-go/fvm/systemcontracts" flowGo "github.com/onflow/flow-go/model/flow" flowkitConfig "github.com/onflow/flowkit/config" @@ -98,17 +98,13 @@ func create( } else { // Ask for project name if not given if len(args) < 1 { - m := prompt.NewTextInput("Enter the name of your project", "Hello World") - finalModel, err := tea.NewProgram(m).Run() - + userInput, err := prompt.RunTextInput("Enter the name of your project", "Type your project name here...") if err != nil { - fmt.Printf("Error running program: %v\n", err) + fmt.Printf("Error running project name: %v\n", err) os.Exit(1) } - name := finalModel.(prompt.TextInputModel).GetValue() - - targetDir, err = getTargetDirectory(name) + targetDir, err = getTargetDirectory(userInput) if err != nil { return nil, err } @@ -156,7 +152,7 @@ func create( // Prompt to ask which core contracts should be installed sc := systemcontracts.SystemContractsForChain(flowGo.Mainnet) - promptMessage := "Select the core contracts you'd like to install" + promptMessage := "Select the core contracts you'd like to install:" contractNames := make([]string, 0) @@ -164,21 +160,17 @@ func create( contractNames = append(contractNames, contract.Name) } - m := prompt.SelectOptions(contractNames, promptMessage) - finalModel, err := tea.NewProgram(m).Run() - + selectedContractNames, err := prompt.RunSelectOptions(contractNames, promptMessage) if err != nil { - fmt.Printf("Error running program: %v\n", err) + fmt.Printf("Error running dependency selection: %v\n", err) os.Exit(1) } - final := finalModel.(prompt.OptionSelectModel) - var dependencies []flowkitConfig.Dependency // Loop standard contracts and add them to the dependencies if selected - for i, contract := range sc.All() { - if _, ok := final.Selected[i]; ok { + for _, contract := range sc.All() { + if slices.Contains(selectedContractNames, contract.Name) { dependencies = append(dependencies, flowkitConfig.Dependency{ Name: contract.Name, Source: flowkitConfig.Source{ From 4d4773c8bdca05c4c3978baa45bf4a41705962b6 Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:33:23 -0700 Subject: [PATCH 4/6] Make models private --- internal/prompt/select-options.go | 46 +++++++++++++++---------------- internal/prompt/text-input.go | 25 +++++++++-------- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/internal/prompt/select-options.go b/internal/prompt/select-options.go index 029249229..63c4c06e1 100644 --- a/internal/prompt/select-options.go +++ b/internal/prompt/select-options.go @@ -25,28 +25,28 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -// OptionSelectModel represents the prompt state -type OptionSelectModel struct { +// optionSelectModel represents the prompt state but is now private +type optionSelectModel struct { message string // message to display cursor int // position of the cursor - Choices []string // items on the list - Selected map[int]struct{} // which items are selected + choices []string // items on the list + selected map[int]struct{} // which items are selected } -// SelectOptions creates a prompt for selecting multiple options -func SelectOptions(options []string, message string) OptionSelectModel { - return OptionSelectModel{ +// selectOptions creates a prompt for selecting multiple options but is now private +func selectOptions(options []string, message string) optionSelectModel { + return optionSelectModel{ message: message, - Choices: options, - Selected: make(map[int]struct{}), + choices: options, + selected: make(map[int]struct{}), } } -func (m OptionSelectModel) Init() tea.Cmd { +func (m optionSelectModel) Init() tea.Cmd { return nil // No initial command } -func (m OptionSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m optionSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { @@ -59,16 +59,16 @@ func (m OptionSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case tea.KeyDown: // Navigate down - if m.cursor < len(m.Choices)-1 { + if m.cursor < len(m.choices)-1 { m.cursor++ } case tea.KeySpace: // Select an item // Toggle selection - if _, ok := m.Selected[m.cursor]; ok { - delete(m.Selected, m.cursor) // Deselect + if _, ok := m.selected[m.cursor]; ok { + delete(m.selected, m.cursor) // Deselect } else { - m.Selected[m.cursor] = struct{}{} // Select + m.selected[m.cursor] = struct{}{} // Select } case tea.KeyEnter: // Confirm selection @@ -79,18 +79,18 @@ func (m OptionSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m OptionSelectModel) View() string { +func (m optionSelectModel) View() string { var b strings.Builder b.WriteString(fmt.Sprintf("%s.\n", m.message)) b.WriteString("Use arrow keys to navigate, space to select, enter to confirm, q to quit:\n\n") - for i, choice := range m.Choices { + for i, choice := range m.choices { if m.cursor == i { b.WriteString("> ") } else { b.WriteString(" ") } // Mark selected items - if _, ok := m.Selected[i]; ok { + if _, ok := m.selected[i]; ok { b.WriteString("[x] ") } else { b.WriteString("[ ] ") @@ -100,19 +100,19 @@ func (m OptionSelectModel) View() string { return b.String() } -// RunSelectOptions handles creating, running the select options prompt, and returning selected choices +// RunSelectOptions remains public and is the interface for external usage. func RunSelectOptions(options []string, message string) ([]string, error) { - model := SelectOptions(options, message) + model := selectOptions(options, message) p := tea.NewProgram(model) finalModel, err := p.Run() if err != nil { return nil, err } - final := finalModel.(OptionSelectModel) + final := finalModel.(optionSelectModel) selectedChoices := make([]string, 0) - for i := range final.Selected { - selectedChoices = append(selectedChoices, final.Choices[i]) + for i := range final.selected { + selectedChoices = append(selectedChoices, final.choices[i]) } return selectedChoices, nil } diff --git a/internal/prompt/text-input.go b/internal/prompt/text-input.go index 7889ea5f5..147e78b19 100644 --- a/internal/prompt/text-input.go +++ b/internal/prompt/text-input.go @@ -6,31 +6,32 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -type TextInputModel struct { +// textInputModel is now private, only accessible within the 'prompt' package. +type textInputModel struct { textInput textinput.Model err error customMsg string } -// NewTextInput initializes a new text input model with a custom message -func NewTextInput(customMsg, placeholder string) TextInputModel { +// newTextInput is a private function that initializes a new text input model. +func newTextInput(customMsg, placeholder string) textInputModel { ti := textinput.New() ti.Placeholder = placeholder ti.Focus() ti.CharLimit = 256 ti.Width = 30 - return TextInputModel{ + return textInputModel{ textInput: ti, customMsg: customMsg, } } -func (m TextInputModel) Init() tea.Cmd { +func (m textInputModel) Init() tea.Cmd { return textinput.Blink } -func (m TextInputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m textInputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { @@ -45,19 +46,19 @@ func (m TextInputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m TextInputModel) View() string { +func (m textInputModel) View() string { return fmt.Sprintf("%s\n\n%s\n\n%s", m.customMsg, m.textInput.View(), "(Enter to submit, Esc to quit)") } -// RunTextInput handles running the text input and retrieving the result +// RunTextInput remains public. It's the entry point for external usage. func RunTextInput(customMsg, placeholder string) (string, error) { - model := NewTextInput(customMsg, placeholder) + model := newTextInput(customMsg, placeholder) p := tea.NewProgram(model) if finalModel, err := p.Run(); err != nil { - return "", err // return the error to handle it outside if necessary + return "", err } else { - final := finalModel.(TextInputModel) - return final.textInput.Value(), nil // directly return the input value + final := finalModel.(textInputModel) + return final.textInput.Value(), nil } } From 274a69884c5eb516b6fe5ae7de3afe724eb3a8f4 Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:41:54 -0700 Subject: [PATCH 5/6] Fix imports --- internal/prompt/text-input.go | 1 + internal/super/setup.go | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/prompt/text-input.go b/internal/prompt/text-input.go index 147e78b19..ccb88cce3 100644 --- a/internal/prompt/text-input.go +++ b/internal/prompt/text-input.go @@ -2,6 +2,7 @@ package prompt import ( "fmt" + "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" ) diff --git a/internal/super/setup.go b/internal/super/setup.go index cedb04fa8..e1ce6c2b5 100644 --- a/internal/super/setup.go +++ b/internal/super/setup.go @@ -21,12 +21,13 @@ package super import ( "bytes" "fmt" - flowsdk "github.com/onflow/flow-go-sdk" - "golang.org/x/exp/slices" "io" "os" "path/filepath" + flowsdk "github.com/onflow/flow-go-sdk" + "golang.org/x/exp/slices" + "github.com/onflow/flow-cli/internal/prompt" "github.com/onflow/flow-go/fvm/systemcontracts" From 6fe02787d88a01db6ef2670f180135ace770635e Mon Sep 17 00:00:00 2001 From: Chase Fleming <1666730+chasefleming@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:46:10 -0700 Subject: [PATCH 6/6] Add header --- internal/prompt/text-input.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/internal/prompt/text-input.go b/internal/prompt/text-input.go index ccb88cce3..16e89b22c 100644 --- a/internal/prompt/text-input.go +++ b/internal/prompt/text-input.go @@ -1,3 +1,21 @@ +/* + * Flow CLI + * + * Copyright 2019 Dapper Labs, Inc. + * + * 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 + * + * http://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 prompt import (