diff --git a/cmd/state/internal/cmdtree/config.go b/cmd/state/internal/cmdtree/config.go index e213eca340..d09608e4e4 100644 --- a/cmd/state/internal/cmdtree/config.go +++ b/cmd/state/internal/cmdtree/config.go @@ -10,17 +10,17 @@ import ( func newConfigCommand(prime *primer.Values) *captain.Command { return captain.NewCommand( "config", - locale.Tl("config_title", "Config"), + locale.Tl("config_title", "Listing Configuration Keys and Values"), locale.Tl("config_description", "Manage the State Tool configuration"), prime, []*captain.Flag{}, []*captain.Argument{}, func(ccmd *captain.Command, _ []string) error { - runner, err := config.NewConfig(prime) + runner, err := config.NewList(prime) if err != nil { return err } - return runner.Run(ccmd.Usage) + return runner.Run() }).SetGroup(UtilsGroup).SetSupportsStructuredOutput() } diff --git a/internal/colorize/colorize.go b/internal/colorize/colorize.go index 9fdd9b06c5..c351727f6c 100644 --- a/internal/colorize/colorize.go +++ b/internal/colorize/colorize.go @@ -180,6 +180,8 @@ func colorize(ct ColorTheme, writer io.Writer, arg string) { ct.Warning(writer) case `ERROR`: ct.Error(writer) + case `BOLD`: + ct.Bold(writer) case `DISABLED`: ct.Disabled(writer) case `ACTIONABLE`: diff --git a/internal/locale/locales/en-us.yaml b/internal/locale/locales/en-us.yaml index 50558aa40b..a23b5d66b3 100644 --- a/internal/locale/locales/en-us.yaml +++ b/internal/locale/locales/en-us.yaml @@ -1518,6 +1518,12 @@ version: other: Version license: other: License +key: + other: Key +value: + other: Value +default: + other: Default vulnerabilities: other: Vulnerabilities (CVEs) dependency_row: @@ -1560,3 +1566,7 @@ install_report_updated: other: "Updated: [NOTICE]{{.V0}}[/RESET]" install_report_removed: other: "Removed: [NOTICE]{{.V0}}[/RESET]" +config_get_help: + other: "To GET the value for a specific config key run '[ACTIONABLE]state config get [/RESET]'" +config_set_help: + other: "To SET the value for a specific config key run '[ACTIONABLE]state config set [/RESET]'" diff --git a/internal/mediators/config/registry.go b/internal/mediators/config/registry.go index 526a641c88..f6f327f53d 100644 --- a/internal/mediators/config/registry.go +++ b/internal/mediators/config/registry.go @@ -1,5 +1,7 @@ package config +import "sort" + type Type int const ( @@ -29,6 +31,7 @@ type Option struct { GetEvent Event SetEvent Event isRegistered bool + isHidden bool } type Registry map[string]Option @@ -49,19 +52,28 @@ func NewEnum(options []string, default_ string) *Enums { func GetOption(key string) Option { rule, ok := registry[key] if !ok { - return Option{key, String, "", EmptyEvent, EmptyEvent, false} + return Option{key, String, "", EmptyEvent, EmptyEvent, false, false} } return rule } // Registers a config option without get/set events. func RegisterOption(key string, t Type, defaultValue interface{}) { - RegisterOptionWithEvents(key, t, defaultValue, EmptyEvent, EmptyEvent) + registerOption(key, t, defaultValue, EmptyEvent, EmptyEvent, false) +} + +// Registers a hidden config option without get/set events. +func RegisterHiddenOption(key string, t Type, defaultValue interface{}) { + registerOption(key, t, defaultValue, EmptyEvent, EmptyEvent, true) } // Registers a config option with get/set events. func RegisterOptionWithEvents(key string, t Type, defaultValue interface{}, get, set Event) { - registry[key] = Option{key, t, defaultValue, get, set, true} + registerOption(key, t, defaultValue, get, set, false) +} + +func registerOption(key string, t Type, defaultValue interface{}, get, set Event, hidden bool) { + registry[key] = Option{key, t, defaultValue, get, set, true, hidden} } func KnownOption(rule Option) bool { @@ -74,3 +86,18 @@ func GetDefault(opt Option) interface{} { } return opt.Default } + +// Registered returns all registered options, excluding hidden ones +func Registered() []Option { + var opts []Option + for _, opt := range registry { + if opt.isHidden { + continue + } + opts = append(opts, opt) + } + sort.SliceStable(opts, func(i, j int) bool { + return opts[i].Name < opts[j].Name + }) + return opts +} diff --git a/internal/runbits/runtime/runtime.go b/internal/runbits/runtime/runtime.go index 8a1256ed99..f4b72ba59b 100644 --- a/internal/runbits/runtime/runtime.go +++ b/internal/runbits/runtime/runtime.go @@ -35,7 +35,7 @@ import ( ) func init() { - configMediator.RegisterOption(constants.AsyncRuntimeConfig, configMediator.Bool, false) + configMediator.RegisterHiddenOption(constants.AsyncRuntimeConfig, configMediator.Bool, false) } type Opts struct { diff --git a/internal/runners/config/config.go b/internal/runners/config/config.go index 9734b6553d..689b40e7ef 100644 --- a/internal/runners/config/config.go +++ b/internal/runners/config/config.go @@ -1,14 +1,19 @@ package config import ( + "fmt" + "strings" + "github.com/ActiveState/cli/internal/config" + "github.com/ActiveState/cli/internal/locale" + mediator "github.com/ActiveState/cli/internal/mediators/config" "github.com/ActiveState/cli/internal/output" "github.com/ActiveState/cli/internal/primer" + "github.com/ActiveState/cli/internal/table" ) -type Config struct { - out output.Outputer - cfg *config.Instance +type List struct { + prime primeable } type primeable interface { @@ -18,13 +23,110 @@ type primeable interface { primer.Analyticer } -func NewConfig(prime primeable) (*Config, error) { - return &Config{ - out: prime.Output(), - cfg: prime.Config(), +func NewList(prime primeable) (*List, error) { + return &List{ + prime: prime, }, nil } -func (c *Config) Run(usageFunc func() error) error { - return usageFunc() +type structuredConfigData struct { + Key string `json:"key"` + Value interface{} `json:"value"` + Default interface{} `json:"default"` + opt mediator.Option +} + +func (c *List) Run() error { + registered := mediator.Registered() + + cfg := c.prime.Config() + out := c.prime.Output() + + var data []structuredConfigData + for _, opt := range registered { + configuredValue := cfg.Get(opt.Name) + data = append(data, structuredConfigData{ + Key: opt.Name, + Value: configuredValue, + Default: mediator.GetDefault(opt), + opt: opt, + }) + } + + if out.Type().IsStructured() { + out.Print(output.Structured(data)) + } else { + if err := c.renderUserFacing(data); err != nil { + return err + } + } + + return nil +} + +func (c *List) renderUserFacing(configData []structuredConfigData) error { + cfg := c.prime.Config() + out := c.prime.Output() + + tbl := table.New(locale.Ts("key", "value", "default")) + tbl.HideDash = true + for _, config := range configData { + tbl.AddRow([]string{ + fmt.Sprintf("[CYAN]%s[/RESET]", config.Key), + renderConfigValue(cfg, config.opt), + fmt.Sprintf("[DISABLED]%s[/RESET]", formatValue(config.opt, config.Default)), + }) + } + + out.Print(tbl.Render()) + out.Notice("") + out.Notice(locale.T("config_get_help")) + out.Notice(locale.T("config_set_help")) + + return nil +} + +func renderConfigValue(cfg *config.Instance, opt mediator.Option) string { + configured := cfg.Get(opt.Name) + var tags []string + if opt.Type == mediator.Bool { + if configured == true { + tags = append(tags, "[GREEN]") + } else { + tags = append(tags, "[RED]") + } + } + + value := formatValue(opt, configured) + if cfg.IsSet(opt.Name) { + tags = append(tags, "[BOLD]") + value = value + "*" + } + + if len(tags) > 0 { + return fmt.Sprintf("%s%s[/RESET]", strings.Join(tags, ""), value) + } + + return value +} + +func formatValue(opt mediator.Option, value interface{}) string { + switch opt.Type { + case mediator.Enum, mediator.String: + return formatString(fmt.Sprintf("%v", value)) + default: + return fmt.Sprintf("%v", value) + } +} + +func formatString(value string) string { + if value == "" { + return "\"\"" + } + + if len(value) > 100 { + value = value[:100] + "..." + } + + return fmt.Sprintf("\"%s\"", value) } diff --git a/test/integration/config_int_test.go b/test/integration/config_int_test.go index db1bb3dfe9..df2021309e 100644 --- a/test/integration/config_int_test.go +++ b/test/integration/config_int_test.go @@ -88,6 +88,33 @@ func (suite *ConfigIntegrationTestSuite) TestJSON() { AssertValidJSON(suite.T(), cp) } +func (suite *ConfigIntegrationTestSuite) TestList() { + suite.OnlyRunForTags(tagsuite.Config) + ts := e2e.New(suite.T(), false) + defer ts.Close() + + cp := ts.Spawn("config") + cp.Expect("Key") + cp.Expect("Value") + cp.Expect("Default") + cp.Expect("optin.buildscripts") + cp.Expect("false") + cp.ExpectExitCode(0) + + cp = ts.Spawn("config", "set", "optin.buildscripts", "true") + cp.Expect("Successfully") + cp.ExpectExitCode(0) + + cp = ts.Spawn("config") + cp.Expect("Key") + cp.Expect("Value") + cp.Expect("Default") + cp.Expect("optin.buildscripts") + cp.Expect("true*") + cp.ExpectExitCode(0) + + suite.Require().NotContains(cp.Snapshot(), constants.AsyncRuntimeConfig) +} func TestConfigIntegrationTestSuite(t *testing.T) { suite.Run(t, new(ConfigIntegrationTestSuite)) }