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

Commit

Permalink
Add Bubbletea list for command selecting
Browse files Browse the repository at this point in the history
Signed-off-by: zychen5186 <[email protected]>
  • Loading branch information
zychen5186 committed Apr 27, 2024
1 parent 46c6751 commit ed9b556
Show file tree
Hide file tree
Showing 7 changed files with 473 additions and 0 deletions.
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 @@ import (
"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 @@ Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
`)

bubbletea.ShowCmdList(rootCmd)

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"}
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) {
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{})
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

0 comments on commit ed9b556

Please sign in to comment.