diff --git a/console/service/index.go b/console/service/index.go index 893cde3..ed52328 100644 --- a/console/service/index.go +++ b/console/service/index.go @@ -1,10 +1,19 @@ package service import ( - "github.com/confetti-framework/contract/inter" + "fmt" "sort" + "strings" + + "github.com/confetti-framework/contract/inter" + "github.com/jedib0t/go-pretty/v6/table" ) +var globalOptions = map[string]string{ + "-h --help": "Show the command's available arguments.", + "--env-file": "Run the command with a defined environment file.", +} + func RenderIndex(c inter.Cli, commands []inter.Command) inter.ExitCode { // Add title and instruction for global usage name := c.App().Make("config.App.Name").(string) @@ -16,17 +25,69 @@ func RenderIndex(c inter.Cli, commands []inter.Command) inter.ExitCode { }) t := c.Table() + t.AppendRow([]interface{}{"\u001B[32mGlobal options:\u001B[0m"}) - t.AppendRow([]interface{}{"-h --help", "Show the command's available arguments."}) - t.AppendRow([]interface{}{"--env-file", "Run the command with a defined environment file."}) + printGlobalOptions(t) t.AppendRow([]interface{}{"\n\u001B[32mAvailable commands:\u001B[0m"}) + printCommandsByGroup(t, commands) + + t.Render() + return inter.Success +} + +func printGlobalOptions(t table.Writer) { + for flag, description := range globalOptions { + t.AppendRow([]interface{}{" " + flag, description}) + } +} + +func printCommandsByGroup(t table.Writer, commands []inter.Command) { + // Commands without a group for _, command := range commands { - t.AppendRow([]interface{}{command.Name(), command.Description()}) + if !strings.ContainsRune(command.Name(), ':') { + t.AppendRow([]interface{}{fmt.Sprintf(" \u001B[33m%s\u001B[0m", command.Name()), command.Description()}) + } } - t.Render() + // Commands with a group (e.g. app:serve) + for _, groupName := range getGroupNames(commands) { + t.AppendRow([]interface{}{fmt.Sprintf(" \u001B[32m%s\u001B[0m", groupName)}) - return inter.Success + for _, command := range commands { + if strings.HasPrefix(command.Name(), groupName+":") { + t.AppendRow([]interface{}{fmt.Sprintf(" \u001B[33m%s\u001B[0m", command.Name()), command.Description()}) + } + } + } +} + +func getGroupNames(commands []inter.Command) (names []string) { + groups := map[string]bool{} + + // Find all group names (unique!) + for _, command := range commands { + parts := strings.Split(command.Name(), ":") + + if len(parts) <= 1 { + continue + } + + groupName := parts[0] + + groups[groupName] = true + } + + // Reduce to keys + for groupName := range groups { + names = append(names, groupName) + } + + // Ensure groups are sorted, just like commands + sort.SliceStable(names, func(i, j int) bool { + return names[i] < names[j] + }) + + return } diff --git a/test/console/cast_options_test.go b/test/console/cast_options_test.go index 27f0725..4bb948d 100644 --- a/test/console/cast_options_test.go +++ b/test/console/cast_options_test.go @@ -3,15 +3,16 @@ package console import ( "bytes" "fmt" + "strconv" + "testing" + "time" + "github.com/confetti-framework/contract/inter" "github.com/confetti-framework/foundation" "github.com/confetti-framework/foundation/console" "github.com/confetti-framework/foundation/console/facade" "github.com/spf13/cast" "github.com/stretchr/testify/require" - "strconv" - "testing" - "time" ) type structWithOptionBool struct { @@ -67,7 +68,7 @@ func Test_cast_option_bool_true(t *testing.T) { Commands: []inter.Command{structWithOptionBool{}}, }.Handle() - require.Contains(t, output.String(), `true`) + require.Contains(t, output.String(), `true`) } type structWithOptionString struct { @@ -249,10 +250,16 @@ func Test_cast_flag_with_dash(t *testing.T) { } type structWithDescription struct { - DryRun bool `short:"dr" flag:"dry-run" description:"The flag description"` + DryRun bool `short:"dr" flag:"dry-run" description:"The flag description"` + CommandName string } -func (s structWithDescription) Name() string { return "test" } +func (s structWithDescription) Name() string { + if s.CommandName == "" { + return "test" + } + return s.CommandName +} func (s structWithDescription) Description() string { return "test" } func (s structWithDescription) Handle(_ inter.Cli) inter.ExitCode { return inter.Success diff --git a/test/console/options_test.go b/test/console/options_test.go index 005e604..12ee51b 100644 --- a/test/console/options_test.go +++ b/test/console/options_test.go @@ -1,12 +1,13 @@ package console import ( + "testing" + "github.com/confetti-framework/contract/inter" "github.com/confetti-framework/foundation/console" "github.com/confetti-framework/foundation/console/service" "github.com/confetti-framework/support" "github.com/stretchr/testify/require" - "testing" ) type mockCommandWithoutOptions struct{} @@ -22,7 +23,30 @@ func Test_show_index_if_no_command(t *testing.T) { }.Handle() require.Equal(t, inter.Success, code) - require.Contains(t, TrimDoubleSpaces(output.String()), "\n Confetti\x1b[39m\n\n") + result := TrimDoubleSpaces(output.String()) + require.Contains(t, result, "\n Confetti\x1b[39m\n\n") + + require.Contains(t, result, "\n \x1b[32mGlobal options:\x1b[0m") + require.Contains(t, result, "-h --help Show the command's available arguments.") + require.Contains(t, result, "--env-file Run the command with a defined environment file.") +} + +func Test_show_index_with_groups(t *testing.T) { + output, app := setUp() + app.Bind("config.App.OsArgs", []interface{}{"/exe/main"}) + + code := console.Kernel{ + App: app, + Writer: &output, + Commands: []inter.Command{structWithDescription{CommandName: "make:test"}}, + }.Handle() + + require.Equal(t, inter.Success, code) + result := TrimDoubleSpaces(output.String()) + + require.Contains(t, result, "\x1b[32mAvailable commands:\x1b[0m") + require.Contains(t, result, "\x1b[33mbaker\x1b[0m Interact with your application.") + require.Contains(t, result, "\n \x1b[32mmake\x1b[0m\n \x1b[33mmake:test\x1b[0m test\n") } func Test_get_option_from_command_without_options(t *testing.T) {