Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add generic text input prompt and refactor #1527

Merged
merged 6 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
53 changes: 35 additions & 18 deletions internal/prompt/select-options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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("[ ] ")
Expand All @@ -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
}
83 changes: 83 additions & 0 deletions internal/prompt/text-input.go
Original file line number Diff line number Diff line change
@@ -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
}
}
28 changes: 15 additions & 13 deletions internal/super/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -147,29 +153,25 @@ 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)

for _, contract := range sc.All() {
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{
Expand Down
Loading