This repository has been archived by the owner on May 31, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 83
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Bubbletea list for command selecting
Signed-off-by: zychen5186 <[email protected]>
- Loading branch information
1 parent
46c6751
commit ed9b556
Showing
7 changed files
with
473 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:]) | ||
// } |
Oops, something went wrong.