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/select-options.go b/internal/prompt/select-options.go index 2d15a69ae..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("[ ] ") @@ -99,3 +99,20 @@ func (m OptionSelectModel) View() string { } return b.String() } + +// RunSelectOptions remains public and is the interface for external usage. +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 new file mode 100644 index 000000000..16e89b22c --- /dev/null +++ b/internal/prompt/text-input.go @@ -0,0 +1,83 @@ +/* + * 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 ( + "fmt" + + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" +) + +// textInputModel is now private, only accessible within the 'prompt' package. +type textInputModel struct { + textInput textinput.Model + err error + customMsg string +} + +// 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{ + 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)") +} + +// RunTextInput remains public. It's the entry point for external usage. +func RunTextInput(customMsg, placeholder string) (string, error) { + model := newTextInput(customMsg, placeholder) + p := tea.NewProgram(model) + + if finalModel, err := p.Run(); err != nil { + return "", err + } else { + final := finalModel.(textInputModel) + return final.textInput.Value(), nil + } +} diff --git a/internal/super/setup.go b/internal/super/setup.go index c9f5d230f..e1ce6c2b5 100644 --- a/internal/super/setup.go +++ b/internal/super/setup.go @@ -25,10 +25,11 @@ import ( "os" "path/filepath" + flowsdk "github.com/onflow/flow-go-sdk" + "golang.org/x/exp/slices" + "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,8 +99,13 @@ func create( } else { // Ask for project name if not given if len(args) < 1 { - name := prompt.NamePrompt() - targetDir, err = getTargetDirectory(name) + userInput, err := prompt.RunTextInput("Enter the name of your project", "Type your project name here...") + if err != nil { + fmt.Printf("Error running project name: %v\n", err) + os.Exit(1) + } + + targetDir, err = getTargetDirectory(userInput) if err != nil { return nil, err } @@ -147,7 +153,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) @@ -155,21 +161,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{