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

Commit ed9b556

Browse files
committed
Add Bubbletea list for command selecting
Signed-off-by: zychen5186 <[email protected]>
1 parent 46c6751 commit ed9b556

File tree

7 files changed

+473
-0
lines changed

7 files changed

+473
-0
lines changed

cmd/root.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/flyteorg/flytectl/cmd/update"
2121
"github.com/flyteorg/flytectl/cmd/upgrade"
2222
"github.com/flyteorg/flytectl/cmd/version"
23+
"github.com/flyteorg/flytectl/pkg/bubbletea"
2324
f "github.com/flyteorg/flytectl/pkg/filesystemutils"
2425
"github.com/flyteorg/flytectl/pkg/printer"
2526

@@ -102,6 +103,8 @@ Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
102103
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
103104
`)
104105

106+
bubbletea.ShowCmdList(rootCmd)
107+
105108
return rootCmd
106109
}
107110

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ require (
6565
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
6666
github.com/Microsoft/go-winio v0.5.0 // indirect
6767
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 // indirect
68+
github.com/atotto/clipboard v0.1.4 // indirect
6869
github.com/aws/aws-sdk-go v1.44.2 // indirect
6970
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
7071
github.com/beorn7/perks v1.0.1 // indirect
@@ -142,6 +143,7 @@ require (
142143
github.com/prometheus/procfs v0.7.3 // indirect
143144
github.com/rivo/uniseg v0.4.7 // indirect
144145
github.com/russross/blackfriday/v2 v2.1.0 // indirect
146+
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
145147
github.com/spf13/afero v1.9.2 // indirect
146148
github.com/spf13/cast v1.4.1 // indirect
147149
github.com/spf13/jwalterweatherman v1.1.0 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY
130130
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
131131
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=
132132
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
133+
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
134+
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
133135
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
134136
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
135137
github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E=
@@ -824,6 +826,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
824826
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
825827
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
826828
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
829+
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=
830+
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
827831
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
828832
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
829833
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=

pkg/bubbletea/bubbletea_list.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package bubbletea
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"strings"
8+
9+
"github.com/charmbracelet/bubbles/list"
10+
tea "github.com/charmbracelet/bubbletea"
11+
"github.com/charmbracelet/lipgloss"
12+
"github.com/spf13/cobra"
13+
)
14+
15+
const (
16+
listHeight = 17
17+
defaultWidth = 40
18+
)
19+
20+
var (
21+
titleStyle = lipgloss.NewStyle().MarginLeft(2)
22+
itemStyle = lipgloss.NewStyle().PaddingLeft(4)
23+
selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170"))
24+
paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4)
25+
helpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1)
26+
quitTextStyle = lipgloss.NewStyle().Margin(0, 0, 0, 0)
27+
)
28+
29+
type item string
30+
31+
func (i item) FilterValue() string { return "" }
32+
33+
type itemDelegate struct{}
34+
35+
func (d itemDelegate) Height() int { return 1 }
36+
func (d itemDelegate) Spacing() int { return 0 }
37+
func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil }
38+
func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
39+
i, ok := listItem.(item)
40+
if !ok {
41+
return
42+
}
43+
44+
str := string(i)
45+
46+
fn := itemStyle.Render
47+
48+
if index == m.Index() {
49+
fn = func(s ...string) string {
50+
return selectedItemStyle.Render("> " + strings.Join(s, " "))
51+
}
52+
}
53+
54+
fmt.Fprint(w, fn(str))
55+
}
56+
57+
type listModel struct {
58+
list list.Model
59+
quitting bool
60+
}
61+
62+
func (m listModel) Init() tea.Cmd {
63+
return nil
64+
}
65+
66+
func (m listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
67+
switch msg := msg.(type) {
68+
case tea.WindowSizeMsg:
69+
m.list.SetWidth(msg.Width)
70+
return m, nil
71+
72+
case tea.KeyMsg:
73+
switch msg.String() {
74+
case "q", "ctrl+c":
75+
m.quitting = true
76+
return m, tea.Quit
77+
78+
case "enter":
79+
item, _ := m.list.SelectedItem().(item)
80+
m, err := genListModel(m, string(item))
81+
if err != nil || m.quitting {
82+
return m, tea.Quit
83+
}
84+
return m, nil
85+
}
86+
}
87+
88+
var cmd tea.Cmd
89+
m.list, cmd = m.list.Update(msg)
90+
return m, cmd
91+
}
92+
93+
func (m listModel) View() string {
94+
if m.quitting {
95+
return quitTextStyle.Render("")
96+
}
97+
return "\n" + m.list.View()
98+
}
99+
100+
func genList(items []list.Item, title string) list.Model {
101+
l := list.New(items, itemDelegate{}, defaultWidth, listHeight)
102+
l.SetShowTitle(false)
103+
l.SetShowStatusBar(false)
104+
l.SetFilteringEnabled(false)
105+
if title != "" {
106+
l.Title = title
107+
l.SetShowTitle(true)
108+
l.Styles.Title = titleStyle
109+
}
110+
l.Styles.PaginationStyle = paginationStyle
111+
l.Styles.HelpStyle = helpStyle
112+
113+
return l
114+
}
115+
116+
func ShowCmdList(_rootCmd *cobra.Command) error {
117+
rootCmd = _rootCmd
118+
119+
currentCmd, run, err := ifRunBubbleTea(*rootCmd)
120+
if err != nil {
121+
return err
122+
}
123+
if !run {
124+
return nil
125+
}
126+
127+
InitCommandFlagMap()
128+
129+
items := generateSubCmdItems(currentCmd)
130+
131+
l := genList(items, "")
132+
m := listModel{list: l}
133+
134+
if _, err := tea.NewProgram(m).Run(); err != nil {
135+
fmt.Println("Error running program:", err)
136+
os.Exit(1)
137+
}
138+
139+
rootCmd.SetArgs(newArgs)
140+
141+
return nil
142+
}
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package bubbletea
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/charmbracelet/bubbles/list"
9+
"github.com/flyteorg/flyte/flyteidl/clients/go/admin"
10+
"github.com/flyteorg/flytectl/cmd/config/subcommand/project"
11+
cmdcore "github.com/flyteorg/flytectl/cmd/core"
12+
"github.com/flyteorg/flytectl/pkg/pkce"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
type Command struct {
17+
Cmd *cobra.Command
18+
Name string
19+
Short string
20+
}
21+
22+
var (
23+
rootCmd *cobra.Command
24+
newArgs []string
25+
flags []string
26+
)
27+
var (
28+
DOMAIN_NAME = [3]string{"development", "staging", "production"}
29+
isCommand = true
30+
nameToCommand = map[string]Command{}
31+
)
32+
33+
// Generate a []list.Item of cmd's subcommands
34+
func generateSubCmdItems(cmd *cobra.Command) []list.Item {
35+
items := []list.Item{}
36+
37+
for _, subcmd := range cmd.Commands() {
38+
subCmdName := strings.Fields(subcmd.Use)[0]
39+
nameToCommand[subCmdName] = Command{
40+
Cmd: subcmd,
41+
Name: subCmdName,
42+
Short: subcmd.Short,
43+
}
44+
items = append(items, item(subCmdName))
45+
}
46+
47+
return items
48+
}
49+
50+
// Generate list.Model for domain names
51+
func genDomainListModel(m listModel) (listModel, error) {
52+
items := []list.Item{}
53+
for _, domain := range DOMAIN_NAME {
54+
items = append(items, item(domain))
55+
}
56+
57+
m.list = genList(items, "Please choose one of the domains")
58+
return m, nil
59+
}
60+
61+
// Get the "get" "project" cobra.Command item
62+
func extractGetProjectCmd() *cobra.Command {
63+
var getProjectCmd *cobra.Command
64+
65+
for _, cmd := range rootCmd.Commands() {
66+
if cmd.Use == "get" {
67+
getProjectCmd = cmd
68+
break
69+
}
70+
}
71+
for _, cmd := range getProjectCmd.Commands() {
72+
if cmd.Use == "project" {
73+
getProjectCmd = cmd
74+
break
75+
}
76+
}
77+
return getProjectCmd
78+
}
79+
80+
// Get all the project names from the configured endpoint
81+
func getProjects(getProjectCmd *cobra.Command) ([]string, error) {
82+
ctx := context.Background()
83+
rootCmd.PersistentPreRunE(rootCmd, []string{})
84+
adminCfg := admin.GetConfig(ctx)
85+
86+
clientSet, err := admin.ClientSetBuilder().WithConfig(admin.GetConfig(ctx)).
87+
WithTokenCache(pkce.TokenCacheKeyringProvider{
88+
ServiceUser: fmt.Sprintf("%s:%s", adminCfg.Endpoint.String(), pkce.KeyRingServiceUser),
89+
ServiceName: pkce.KeyRingServiceName,
90+
}).Build(ctx)
91+
if err != nil {
92+
return nil, err
93+
}
94+
cmdCtx := cmdcore.NewCommandContext(clientSet, getProjectCmd.OutOrStdout())
95+
96+
projects, err := cmdCtx.AdminFetcherExt().ListProjects(ctx, project.DefaultConfig.Filter)
97+
if err != nil {
98+
return nil, err
99+
}
100+
101+
projectNames := []string{}
102+
for _, p := range projects.Projects {
103+
projectNames = append(projectNames, p.Id)
104+
}
105+
106+
return projectNames, nil
107+
}
108+
109+
// Generate list.Model for project names from the configured endpoint
110+
func genProjectListModel(m listModel) (listModel, error) {
111+
getProjectCmd := extractGetProjectCmd()
112+
projects, err := getProjects(getProjectCmd)
113+
if err != nil {
114+
return m, err
115+
}
116+
117+
items := []list.Item{}
118+
for _, project := range projects {
119+
items = append(items, item(project))
120+
}
121+
122+
m.list = genList(items, "Please choose one of the projects")
123+
124+
return m, nil
125+
}
126+
127+
// Generate list.Model of options for different flags
128+
func genFlagListModel(m listModel, f string) (listModel, error) {
129+
var err error
130+
131+
switch f {
132+
case "-p":
133+
m, err = genProjectListModel(m)
134+
case "-d":
135+
m, err = genDomainListModel(m)
136+
}
137+
138+
return m, err
139+
}
140+
141+
// Generate list.Model of subcommands from a given command
142+
func genCmdListModel(m listModel, c string) listModel {
143+
if len(nameToCommand[c].Cmd.Commands()) == 0 {
144+
return m
145+
}
146+
147+
items := generateSubCmdItems(nameToCommand[c].Cmd)
148+
l := genList(items, "")
149+
m.list = l
150+
151+
return m
152+
}
153+
154+
// Generate list.Model after user chose one of the item
155+
func genListModel(m listModel, item string) (listModel, error) {
156+
newArgs = append(newArgs, item)
157+
158+
if isCommand {
159+
m = genCmdListModel(m, item)
160+
var ok bool
161+
if flags, ok = commandFlagMap[sliceToString(newArgs)]; ok { // If found in commandFlagMap means last command
162+
isCommand = false
163+
} else {
164+
return m, nil
165+
}
166+
}
167+
// TODO check if some flags are already input as arguments by user
168+
if len(flags) > 0 {
169+
nextFlag := flags[0]
170+
flags = flags[1:]
171+
newArgs = append(newArgs, nextFlag)
172+
var err error
173+
m, err = genFlagListModel(m, nextFlag)
174+
if err != nil {
175+
return m, err
176+
}
177+
} else {
178+
m.quitting = true
179+
return m, nil
180+
}
181+
return m, nil
182+
}
183+
184+
// func isValidCommand(curArg string, cmd *cobra.Command) (*cobra.Command, bool) {
185+
// for _, subCmd := range cmd.Commands() {
186+
// if subCmd.Use == curArg {
187+
// return subCmd, true
188+
// }
189+
// }
190+
// return nil, false
191+
// }
192+
193+
// func findSubCmdItems(cmd *cobra.Command, inputArgs []string) ([]list.Item, error) {
194+
// if len(inputArgs) == 0 {
195+
// return generateSubCmdItems(cmd), nil
196+
// }
197+
198+
// curArg := inputArgs[0]
199+
// subCmd, isValid := isValidCommand(curArg, cmd)
200+
// if !isValid {
201+
// return nil, fmt.Errorf("not a valid argument: %v", curArg)
202+
// }
203+
204+
// return findSubCmdItems(subCmd, inputArgs[1:])
205+
// }

0 commit comments

Comments
 (0)