Skip to content
This repository has been archived by the owner on May 31, 2024. It is now read-only.

Integrate Bubbletea list CLI for command selecting #477

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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 cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"github.com/flyteorg/flytectl/cmd/update"
"github.com/flyteorg/flytectl/cmd/upgrade"
"github.com/flyteorg/flytectl/cmd/version"
"github.com/flyteorg/flytectl/pkg/bubbletea"
f "github.com/flyteorg/flytectl/pkg/filesystemutils"
"github.com/flyteorg/flytectl/pkg/printer"

Expand Down Expand Up @@ -102,6 +103,8 @@
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
`)

bubbletea.ShowCmdList(rootCmd)

Check failure on line 106 in cmd/root.go

View workflow job for this annotation

GitHub Actions / Lint / Run Lint

Error return value of `bubbletea.ShowCmdList` is not checked (errcheck)

return rootCmd
}

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ require (
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/Microsoft/go-winio v0.5.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go v1.44.2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
Expand Down Expand Up @@ -142,6 +143,7 @@ require (
github.com/prometheus/procfs v0.7.3 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
github.com/spf13/afero v1.9.2 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E=
Expand Down Expand Up @@ -824,6 +826,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
Expand Down
142 changes: 142 additions & 0 deletions pkg/bubbletea/bubbletea_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package bubbletea

import (
"fmt"
"io"
"os"
"strings"

"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
)

const (
listHeight = 17
defaultWidth = 40
)

var (
titleStyle = lipgloss.NewStyle().MarginLeft(2)
itemStyle = lipgloss.NewStyle().PaddingLeft(4)
selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170"))
paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4)
helpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1)
quitTextStyle = lipgloss.NewStyle().Margin(0, 0, 0, 0)
)

type item string

func (i item) FilterValue() string { return "" }

type itemDelegate struct{}

func (d itemDelegate) Height() int { return 1 }
func (d itemDelegate) Spacing() int { return 0 }
func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil }
func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
i, ok := listItem.(item)
if !ok {
return
}

str := string(i)

fn := itemStyle.Render

if index == m.Index() {
fn = func(s ...string) string {
return selectedItemStyle.Render("> " + strings.Join(s, " "))
}
}

fmt.Fprint(w, fn(str))
}

type listModel struct {
list list.Model
quitting bool
}

func (m listModel) Init() tea.Cmd {
return nil
}

func (m listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.list.SetWidth(msg.Width)
return m, nil

case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c":
m.quitting = true
return m, tea.Quit

case "enter":
item, _ := m.list.SelectedItem().(item)
m, err := genListModel(m, string(item))
if err != nil || m.quitting {
return m, tea.Quit
}
return m, nil
}
}

var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}

func (m listModel) View() string {
if m.quitting {
return quitTextStyle.Render("")
}
return "\n" + m.list.View()
}

func genList(items []list.Item, title string) list.Model {
l := list.New(items, itemDelegate{}, defaultWidth, listHeight)
l.SetShowTitle(false)
l.SetShowStatusBar(false)
l.SetFilteringEnabled(false)
if title != "" {
l.Title = title
l.SetShowTitle(true)
l.Styles.Title = titleStyle
}
l.Styles.PaginationStyle = paginationStyle
l.Styles.HelpStyle = helpStyle

return l
}

func ShowCmdList(_rootCmd *cobra.Command) error {
rootCmd = _rootCmd

currentCmd, run, err := ifRunBubbleTea(*rootCmd)
if err != nil {
return err
}
if !run {
return nil
}

InitCommandFlagMap()

items := generateSubCmdItems(currentCmd)

l := genList(items, "")
m := listModel{list: l}

if _, err := tea.NewProgram(m).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}

rootCmd.SetArgs(newArgs)

return nil
}
205 changes: 205 additions & 0 deletions pkg/bubbletea/bubbletea_list_util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package bubbletea

import (
"context"
"fmt"
"strings"

"github.com/charmbracelet/bubbles/list"
"github.com/flyteorg/flyte/flyteidl/clients/go/admin"
"github.com/flyteorg/flytectl/cmd/config/subcommand/project"
cmdcore "github.com/flyteorg/flytectl/cmd/core"
"github.com/flyteorg/flytectl/pkg/pkce"
"github.com/spf13/cobra"
)

type Command struct {
Cmd *cobra.Command
Name string
Short string
}

var (
rootCmd *cobra.Command
newArgs []string
flags []string
)
var (
DOMAIN_NAME = [3]string{"development", "staging", "production"}

Check failure on line 28 in pkg/bubbletea/bubbletea_list_util.go

View workflow job for this annotation

GitHub Actions / Lint / Run Lint

don't use ALL_CAPS in Go names; use CamelCase (golint)
isCommand = true
nameToCommand = map[string]Command{}
)

// Generate a []list.Item of cmd's subcommands
func generateSubCmdItems(cmd *cobra.Command) []list.Item {
items := []list.Item{}

for _, subcmd := range cmd.Commands() {
subCmdName := strings.Fields(subcmd.Use)[0]
nameToCommand[subCmdName] = Command{
Cmd: subcmd,
Name: subCmdName,
Short: subcmd.Short,
}
items = append(items, item(subCmdName))
}

return items
}

// Generate list.Model for domain names
func genDomainListModel(m listModel) (listModel, error) {

Check failure on line 51 in pkg/bubbletea/bubbletea_list_util.go

View workflow job for this annotation

GitHub Actions / Lint / Run Lint

genDomainListModel - result 1 (error) is always nil (unparam)
items := []list.Item{}
for _, domain := range DOMAIN_NAME {
items = append(items, item(domain))
}

m.list = genList(items, "Please choose one of the domains")
return m, nil
}

// Get the "get" "project" cobra.Command item
func extractGetProjectCmd() *cobra.Command {
var getProjectCmd *cobra.Command

for _, cmd := range rootCmd.Commands() {
if cmd.Use == "get" {
getProjectCmd = cmd
break
}
}
for _, cmd := range getProjectCmd.Commands() {
if cmd.Use == "project" {
getProjectCmd = cmd
break
}
}
return getProjectCmd
}

// Get all the project names from the configured endpoint
func getProjects(getProjectCmd *cobra.Command) ([]string, error) {
ctx := context.Background()
rootCmd.PersistentPreRunE(rootCmd, []string{})

Check failure on line 83 in pkg/bubbletea/bubbletea_list_util.go

View workflow job for this annotation

GitHub Actions / Lint / Run Lint

Error return value is not checked (errcheck)
adminCfg := admin.GetConfig(ctx)

clientSet, err := admin.ClientSetBuilder().WithConfig(admin.GetConfig(ctx)).
WithTokenCache(pkce.TokenCacheKeyringProvider{
ServiceUser: fmt.Sprintf("%s:%s", adminCfg.Endpoint.String(), pkce.KeyRingServiceUser),
ServiceName: pkce.KeyRingServiceName,
}).Build(ctx)
if err != nil {
return nil, err
}
cmdCtx := cmdcore.NewCommandContext(clientSet, getProjectCmd.OutOrStdout())

projects, err := cmdCtx.AdminFetcherExt().ListProjects(ctx, project.DefaultConfig.Filter)
if err != nil {
return nil, err
}

projectNames := []string{}
for _, p := range projects.Projects {
projectNames = append(projectNames, p.Id)
}

return projectNames, nil
}

// Generate list.Model for project names from the configured endpoint
func genProjectListModel(m listModel) (listModel, error) {
getProjectCmd := extractGetProjectCmd()
projects, err := getProjects(getProjectCmd)
if err != nil {
return m, err
}

items := []list.Item{}
for _, project := range projects {
items = append(items, item(project))
}

m.list = genList(items, "Please choose one of the projects")

return m, nil
}

// Generate list.Model of options for different flags
func genFlagListModel(m listModel, f string) (listModel, error) {
var err error

switch f {
case "-p":
m, err = genProjectListModel(m)
case "-d":
m, err = genDomainListModel(m)
}

return m, err
}

// Generate list.Model of subcommands from a given command
func genCmdListModel(m listModel, c string) listModel {
if len(nameToCommand[c].Cmd.Commands()) == 0 {
return m
}

items := generateSubCmdItems(nameToCommand[c].Cmd)
l := genList(items, "")
m.list = l

return m
}

// Generate list.Model after user chose one of the item
func genListModel(m listModel, item string) (listModel, error) {
newArgs = append(newArgs, item)

if isCommand {
m = genCmdListModel(m, item)
var ok bool
if flags, ok = commandFlagMap[sliceToString(newArgs)]; ok { // If found in commandFlagMap means last command
isCommand = false
} else {
return m, nil
}
}
// TODO check if some flags are already input as arguments by user
if len(flags) > 0 {
nextFlag := flags[0]
flags = flags[1:]
newArgs = append(newArgs, nextFlag)
var err error
m, err = genFlagListModel(m, nextFlag)
if err != nil {
return m, err
}
} else {
m.quitting = true
return m, nil
}
return m, nil
}

// func isValidCommand(curArg string, cmd *cobra.Command) (*cobra.Command, bool) {
// for _, subCmd := range cmd.Commands() {
// if subCmd.Use == curArg {
// return subCmd, true
// }
// }
// return nil, false
// }

// func findSubCmdItems(cmd *cobra.Command, inputArgs []string) ([]list.Item, error) {
// if len(inputArgs) == 0 {
// return generateSubCmdItems(cmd), nil
// }

// curArg := inputArgs[0]
// subCmd, isValid := isValidCommand(curArg, cmd)
// if !isValid {
// return nil, fmt.Errorf("not a valid argument: %v", curArg)
// }

// return findSubCmdItems(subCmd, inputArgs[1:])
// }
Loading
Loading