From 9c26894e42473901385a5a876c9925c2efa69503 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Tue, 31 Mar 2026 16:08:21 -0400 Subject: [PATCH 1/7] feat: add --accessible flag to huh interactive prompts --- internal/config/config.go | 1 + internal/config/flags.go | 1 + internal/iostreams/forms.go | 3 ++ internal/iostreams/forms_test.go | 48 ++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index 9993d742..ba8037c8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -58,6 +58,7 @@ type Config struct { SlackTestTraceFlag bool TeamFlag string TokenFlag string + Accessible bool NoColor bool // Feature experiments diff --git a/internal/config/flags.go b/internal/config/flags.go index f2180181..d964f083 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -37,6 +37,7 @@ func (c *Config) InitializeGlobalFlags(cmd *cobra.Command) { cmd.PersistentFlags().BoolVarP(&c.DeprecatedDevFlag, "dev", "d", false, "use dev apis") // Can be removed after v0.25.0 cmd.PersistentFlags().StringVarP(&c.DeprecatedWorkspaceFlag, "workspace", "", "", "select workspace or organization by domain name or team ID") cmd.PersistentFlags().StringSliceVarP(&c.ExperimentsFlag, "experiment", "e", nil, "use the experiment(s) in the command") + cmd.PersistentFlags().BoolVarP(&c.Accessible, "accessible", "", false, "use accessible prompts for screen readers") cmd.PersistentFlags().BoolVarP(&c.ForceFlag, "force", "f", false, "ignore warnings and continue executing command") cmd.PersistentFlags().BoolVarP(&c.NoColor, "no-color", "", false, "remove styles and formatting from outputs") cmd.PersistentFlags().BoolVarP(&c.SkipUpdateFlag, "skip-update", "s", false, "skip checking for latest version of CLI") diff --git a/internal/iostreams/forms.go b/internal/iostreams/forms.go index 0ee0b78c..b0a2a8b8 100644 --- a/internal/iostreams/forms.go +++ b/internal/iostreams/forms.go @@ -37,6 +37,9 @@ func newForm(io *IOStreams, field huh.Field) *huh.Form { } else { form = form.WithTheme(style.ThemeSurvey()) } + if io != nil && io.config.Accessible { + form = form.WithAccessible(true) + } return form } diff --git a/internal/iostreams/forms_test.go b/internal/iostreams/forms_test.go index 31df93e2..319c305a 100644 --- a/internal/iostreams/forms_test.go +++ b/internal/iostreams/forms_test.go @@ -415,6 +415,54 @@ func TestFormsUseSlackTheme(t *testing.T) { }) } +func TestFormsAccessible(t *testing.T) { + fsMock := slackdeps.NewFsMock() + osMock := slackdeps.NewOsMock() + osMock.AddDefaultMocks() + cfg := config.NewConfig(fsMock, osMock) + cfg.Accessible = true + io := NewIOStreams(cfg, fsMock, osMock) + + t.Run("select form accepts valid numbered input", func(t *testing.T) { + var selected string + f := buildSelectForm(io, "Pick one", []string{"A", "B", "C"}, SelectPromptConfig{}, &selected) + + var out strings.Builder + err := f.WithOutput(&out).WithInput(strings.NewReader("2\n")).Run() + + assert.NoError(t, err) + assert.Equal(t, "B", selected) + assert.Contains(t, out.String(), "1. A") + assert.Contains(t, out.String(), "2. B") + assert.Contains(t, out.String(), "3. C") + assert.Contains(t, out.String(), "Enter a number between 1 and 3") + }) + + t.Run("confirm form accepts yes/no input", func(t *testing.T) { + var choice bool + f := buildConfirmForm(io, "Continue?", &choice) + + var out strings.Builder + err := f.WithOutput(&out).WithInput(strings.NewReader("y\n")).Run() + + assert.NoError(t, err) + assert.True(t, choice) + assert.Contains(t, out.String(), "Continue?") + }) + + t.Run("input form accepts text input", func(t *testing.T) { + var input string + f := buildInputForm(io, "Name?", InputPromptConfig{}, &input) + + var out strings.Builder + err := f.WithOutput(&out).WithInput(strings.NewReader("my-app\n")).Run() + + assert.NoError(t, err) + assert.Equal(t, "my-app", input) + assert.Contains(t, out.String(), "Name?") + }) +} + func TestFormsUseSurveyTheme(t *testing.T) { t.Run("multi-select uses survey prefix without lipgloss", func(t *testing.T) { var selected []string From 7a747baf6d61640b9a60e7a97a545fe58170dc48 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Mon, 6 Apr 2026 14:24:43 -0400 Subject: [PATCH 2/7] accessible implies no-color --- cmd/root.go | 5 +++++ cmd/root_test.go | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/cmd/root.go b/cmd/root.go index e9282c69..8e09fbc3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -247,6 +247,11 @@ func InitConfig(ctx context.Context, clients *shared.ClientFactory, rootCmd *cob clients.Config.SystemConfig.SetCustomConfigDirPath(clients.Config.ConfigDirFlag) } + // Accessible mode implies no-color + if clients.Config.Accessible { + clients.Config.NoColor = true + } + // Init color and formatting style.ToggleStyles(clients.IO.IsTTY() && !clients.Config.NoColor) style.ToggleSpinner(clients.IO.IsTTY() && !clients.Config.NoColor && !clients.Config.DebugEnabled) diff --git a/cmd/root_test.go b/cmd/root_test.go index 9ca2b472..eeef1e2d 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -21,6 +21,7 @@ import ( "strings" "testing" + "github.com/slackapi/slack-cli/internal/config" "github.com/slackapi/slack-cli/internal/iostreams" "github.com/slackapi/slack-cli/internal/shared" "github.com/slackapi/slack-cli/internal/slackcontext" @@ -194,6 +195,15 @@ func TestVersionFlags(t *testing.T) { assert.True(t, testutil.ContainsSemVer(output), `-v should output the version number but yielded "%s"`, output) } +func Test_AccessibleImpliesNoColor(t *testing.T) { + cfg := &config.Config{Accessible: true} + // Simulate the logic from PersistentPreRunE + if cfg.Accessible { + cfg.NoColor = true + } + assert.True(t, cfg.NoColor, "--accessible should imply --no-color") +} + func Test_NewSuggestion(t *testing.T) { ctx := slackcontext.MockContext(t.Context()) From 6dc0c1a0dfbe471ea5e39d81f657b6c155b76ad0 Mon Sep 17 00:00:00 2001 From: Ale Mercado <104795114+srtaalej@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:06:02 -0400 Subject: [PATCH 3/7] Update cmd/root_test.go Co-authored-by: Eden Zimbelman --- cmd/root_test.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/cmd/root_test.go b/cmd/root_test.go index eeef1e2d..de8d3cfa 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -195,15 +195,6 @@ func TestVersionFlags(t *testing.T) { assert.True(t, testutil.ContainsSemVer(output), `-v should output the version number but yielded "%s"`, output) } -func Test_AccessibleImpliesNoColor(t *testing.T) { - cfg := &config.Config{Accessible: true} - // Simulate the logic from PersistentPreRunE - if cfg.Accessible { - cfg.NoColor = true - } - assert.True(t, cfg.NoColor, "--accessible should imply --no-color") -} - func Test_NewSuggestion(t *testing.T) { ctx := slackcontext.MockContext(t.Context()) From 504bfcf1b70fecf27a0f340f75509b6473b32ccb Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Tue, 7 Apr 2026 13:28:56 -0400 Subject: [PATCH 4/7] add accessible env var support --- cmd/root_test.go | 1 - internal/config/config.go | 1 + internal/config/dotenv.go | 6 ++++++ internal/config/dotenv_test.go | 35 ++++++++++++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/cmd/root_test.go b/cmd/root_test.go index de8d3cfa..9ca2b472 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -21,7 +21,6 @@ import ( "strings" "testing" - "github.com/slackapi/slack-cli/internal/config" "github.com/slackapi/slack-cli/internal/iostreams" "github.com/slackapi/slack-cli/internal/shared" "github.com/slackapi/slack-cli/internal/slackcontext" diff --git a/internal/config/config.go b/internal/config/config.go index ba8037c8..3ad0f37a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -25,6 +25,7 @@ import ( const slackAutoRequestAAAEnv = "SLACK_AUTO_REQUEST_AAA" const slackConfigDirEnv = "SLACK_CONFIG_DIR" const slackDisableTelemetryEnv = "SLACK_DISABLE_TELEMETRY" +const slackAccessibleEnv = "ACCESSIBLE" const slackTestTraceEnv = "SLACK_TEST_TRACE" type Config struct { diff --git a/internal/config/dotenv.go b/internal/config/dotenv.go index 9ae6333a..9cc67014 100644 --- a/internal/config/dotenv.go +++ b/internal/config/dotenv.go @@ -27,6 +27,12 @@ func (c *Config) LoadEnvironmentVariables() error { return nil } + // Load accessible mode from environment variables + var accessible = strings.TrimSpace(c.os.Getenv(slackAccessibleEnv)) + if accessible != "" && accessible != "false" && accessible != "0" { + c.Accessible = true + } + // Load slackTestTraceFlag from environment variables var testTrace = strings.TrimSpace(c.os.Getenv(slackTestTraceEnv)) if testTrace != "" && testTrace != "false" && testTrace != "0" { diff --git a/internal/config/dotenv_test.go b/internal/config/dotenv_test.go index 77d1773b..bbe05a4a 100644 --- a/internal/config/dotenv_test.go +++ b/internal/config/dotenv_test.go @@ -167,6 +167,41 @@ func Test_DotEnv_LoadEnvironmentVariables(t *testing.T) { assert.Equal(t, "", cfg.ConfigDirFlag) }, }, + "ACCESSIBLE=true should set Accessible to true": { + envName: "ACCESSIBLE", + envValue: "true", + assertOnConfig: func(t *testing.T, cfg *Config) { + assert.Equal(t, true, cfg.Accessible) + }, + }, + "ACCESSIBLE=1 should set Accessible to true": { + envName: "ACCESSIBLE", + envValue: "1", + assertOnConfig: func(t *testing.T, cfg *Config) { + assert.Equal(t, true, cfg.Accessible) + }, + }, + "ACCESSIBLE=false should set Accessible to false": { + envName: "ACCESSIBLE", + envValue: "false", + assertOnConfig: func(t *testing.T, cfg *Config) { + assert.Equal(t, false, cfg.Accessible) + }, + }, + "ACCESSIBLE=0 should set Accessible to false": { + envName: "ACCESSIBLE", + envValue: "0", + assertOnConfig: func(t *testing.T, cfg *Config) { + assert.Equal(t, false, cfg.Accessible) + }, + }, + "empty ACCESSIBLE should set Accessible to false": { + envName: "ACCESSIBLE", + envValue: "", + assertOnConfig: func(t *testing.T, cfg *Config) { + assert.Equal(t, false, cfg.Accessible) + }, + }, } for name, tc := range tableTests { From 0417f400a2c23e970b1e36da3c3d4fde3b1511b0 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Tue, 7 Apr 2026 13:36:43 -0400 Subject: [PATCH 5/7] linter --- internal/iostreams/forms_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/iostreams/forms_test.go b/internal/iostreams/forms_test.go index c0bab5f8..d806e0ed 100644 --- a/internal/iostreams/forms_test.go +++ b/internal/iostreams/forms_test.go @@ -460,6 +460,8 @@ func TestFormsAccessible(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "my-app", input) assert.Contains(t, out.String(), "Name?") + }) +} func TestFormsNoColor(t *testing.T) { t.Run("forms use plain theme with no-color", func(t *testing.T) { From 2318e16f921256e334f83a725ec3b40b80d39106 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Tue, 7 Apr 2026 15:22:49 -0400 Subject: [PATCH 6/7] add default placeholders to accessible prompts --- internal/iostreams/forms.go | 14 ++++++++++++-- internal/iostreams/forms_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/internal/iostreams/forms.go b/internal/iostreams/forms.go index 74807b73..f2ec1e1c 100644 --- a/internal/iostreams/forms.go +++ b/internal/iostreams/forms.go @@ -21,6 +21,7 @@ package iostreams import ( "context" "errors" + "fmt" "slices" huh "charm.land/huh/v2" @@ -47,8 +48,12 @@ func newForm(io *IOStreams, field huh.Field) *huh.Form { // buildInputForm constructs an interactive form for text input prompts. func buildInputForm(io *IOStreams, message string, cfg InputPromptConfig, input *string) *huh.Form { + title := message + if io != nil && io.config.Accessible && cfg.Placeholder != "" { + title = fmt.Sprintf("%s (default: %s)", message, cfg.Placeholder) + } field := huh.NewInput(). - Title(message). + Title(title). Prompt(style.Chevron() + " "). Placeholder(cfg.Placeholder). Value(input) @@ -103,8 +108,13 @@ func buildSelectForm(io *IOStreams, msg string, options []string, cfg SelectProm opts = append(opts, huh.NewOption(key, opt)) } + title := msg + if io != nil && io.config.Accessible && len(options) > 0 { + title = fmt.Sprintf("%s (press Enter for 1)", msg) + } + field := huh.NewSelect[string](). - Title(msg). + Title(title). Description(cfg.Help). Options(opts...). Value(selected) diff --git a/internal/iostreams/forms_test.go b/internal/iostreams/forms_test.go index d806e0ed..0282c704 100644 --- a/internal/iostreams/forms_test.go +++ b/internal/iostreams/forms_test.go @@ -438,6 +438,18 @@ func TestFormsAccessible(t *testing.T) { assert.Contains(t, out.String(), "Enter a number between 1 and 3") }) + t.Run("select form shows default hint in accessible mode", func(t *testing.T) { + var selected string + f := buildSelectForm(io, "Pick one", []string{"Alpha", "Beta"}, SelectPromptConfig{}, &selected) + + var out strings.Builder + err := f.WithOutput(&out).WithInput(strings.NewReader("\n")).Run() + + assert.NoError(t, err) + assert.Equal(t, "Alpha", selected) + assert.Contains(t, out.String(), `Pick one (press Enter for "Alpha")`) + }) + t.Run("confirm form accepts yes/no input", func(t *testing.T) { var choice bool f := buildConfirmForm(io, "Continue?", &choice) @@ -461,6 +473,18 @@ func TestFormsAccessible(t *testing.T) { assert.Equal(t, "my-app", input) assert.Contains(t, out.String(), "Name?") }) + + t.Run("input form shows default placeholder in accessible mode", func(t *testing.T) { + var input string + f := buildInputForm(io, "Name your app:", InputPromptConfig{Placeholder: "cool-app-123"}, &input) + + var out strings.Builder + err := f.WithOutput(&out).WithInput(strings.NewReader("\n")).Run() + + assert.NoError(t, err) + assert.Equal(t, "", input) + assert.Contains(t, out.String(), "Name your app: (default: cool-app-123)") + }) } func TestFormsNoColor(t *testing.T) { From 95a713e76e9a69f0737e2625ed8e105390f2d7b8 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Wed, 8 Apr 2026 13:57:20 -0400 Subject: [PATCH 7/7] tests --- internal/iostreams/forms_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/iostreams/forms_test.go b/internal/iostreams/forms_test.go index 0282c704..5413c18a 100644 --- a/internal/iostreams/forms_test.go +++ b/internal/iostreams/forms_test.go @@ -447,7 +447,7 @@ func TestFormsAccessible(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "Alpha", selected) - assert.Contains(t, out.String(), `Pick one (press Enter for "Alpha")`) + assert.Contains(t, out.String(), "Pick one (press Enter for 1)") }) t.Run("confirm form accepts yes/no input", func(t *testing.T) {