Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9c26894
feat: add --accessible flag to huh interactive prompts
srtaalej Mar 31, 2026
7a747ba
accessible implies no-color
srtaalej Apr 6, 2026
f39d175
Merge branch 'main' into ale-accessibility-flag
srtaalej Apr 6, 2026
6dc0c1a
Update cmd/root_test.go
srtaalej Apr 7, 2026
504bfcf
add accessible env var support
srtaalej Apr 7, 2026
09c76d0
Merge branch 'main' into ale-accessibility-flag
srtaalej Apr 7, 2026
0417f40
linter
srtaalej Apr 7, 2026
907f5ec
Merge branch 'main' into ale-accessibility-flag
srtaalej Apr 7, 2026
2318e16
add default placeholders to accessible prompts
srtaalej Apr 7, 2026
95a713e
tests
srtaalej Apr 8, 2026
4f13099
Merge branch 'main' into ale-accessibility-flag
zimeg Apr 11, 2026
54996c9
Update internal/iostreams/forms.go
srtaalej Apr 13, 2026
da077ee
Update internal/iostreams/forms.go
srtaalej Apr 13, 2026
9652400
fix: add missing strings import and update test assertion
srtaalej May 5, 2026
25927f7
chore: retrigger CI
srtaalej May 5, 2026
f53744f
Merge branch 'main' into ale-accessibility-flag
srtaalej May 5, 2026
03778b3
Update internal/config/config.go
srtaalej May 6, 2026
5ce2834
fix: address review feedback - rename AccessibleFlag, colon suffix logic
srtaalej May 7, 2026
d963438
fix: move AccessibleFlag to alphabetical order in Config struct
srtaalej May 7, 2026
ebd44b3
Merge branch 'main' into ale-accessibility-flag
srtaalej May 7, 2026
906ecde
fix: alphabetical ordering for constants, struct fields, and flags
srtaalej May 8, 2026
cc50a5d
Merge branch 'main' into ale-accessibility-flag
srtaalej May 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.AccessibleFlag {
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)
Expand Down
4 changes: 3 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
)

// Environment Variable constants
const slackAccessibleEnv = "ACCESSIBLE"
const slackAutoRequestAAAEnv = "SLACK_AUTO_REQUEST_AAA"
const slackConfigDirEnv = "SLACK_CONFIG_DIR"
const slackDisableTelemetryEnv = "SLACK_DISABLE_TELEMETRY"
Expand All @@ -38,6 +39,7 @@ type Config struct {
// Raw flags (for metrics)
RawFlags []string
// Command flags
AccessibleFlag bool
APIHostFlag string
APIHostResolved string
AppFlag string
Expand All @@ -50,6 +52,7 @@ type Config struct {
DisableTelemetryFlag bool
ForceFlag bool
LogstashHostResolved string
NoColor bool
RuntimeFlag string
RuntimeName string
RuntimeVersion string
Expand All @@ -58,7 +61,6 @@ type Config struct {
SlackTestTraceFlag bool
TeamFlag string
TokenFlag string
NoColor bool

// Feature experiments
ExperimentsFlag []string
Expand Down
6 changes: 6 additions & 0 deletions internal/config/dotenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.AccessibleFlag = true
}

// Load slackTestTraceFlag from environment variables
var testTrace = strings.TrimSpace(c.os.Getenv(slackTestTraceEnv))
if testTrace != "" && testTrace != "false" && testTrace != "0" {
Expand Down
35 changes: 35 additions & 0 deletions internal/config/dotenv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.AccessibleFlag)
},
},
"ACCESSIBLE=1 should set Accessible to true": {
envName: "ACCESSIBLE",
envValue: "1",
assertOnConfig: func(t *testing.T, cfg *Config) {
assert.Equal(t, true, cfg.AccessibleFlag)
},
},
"ACCESSIBLE=false should set Accessible to false": {
envName: "ACCESSIBLE",
envValue: "false",
assertOnConfig: func(t *testing.T, cfg *Config) {
assert.Equal(t, false, cfg.AccessibleFlag)
},
},
"ACCESSIBLE=0 should set Accessible to false": {
envName: "ACCESSIBLE",
envValue: "0",
assertOnConfig: func(t *testing.T, cfg *Config) {
assert.Equal(t, false, cfg.AccessibleFlag)
},
},
"empty ACCESSIBLE should set Accessible to false": {
envName: "ACCESSIBLE",
envValue: "",
assertOnConfig: func(t *testing.T, cfg *Config) {
assert.Equal(t, false, cfg.AccessibleFlag)
},
},
}

for name, tc := range tableTests {
Expand Down
5 changes: 3 additions & 2 deletions internal/config/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,23 @@ func (c *Config) SetFlags(cmd *cobra.Command) {

// InitializeGlobalFlags configures flags and creates links from cmd to config
func (c *Config) InitializeGlobalFlags(cmd *cobra.Command) {
cmd.PersistentFlags().BoolVarP(&c.AccessibleFlag, "accessible", "", false, "use accessible prompts for screen readers")
cmd.PersistentFlags().StringVar(&c.APIHostFlag, "apihost", "", "Slack API host")
cmd.PersistentFlags().StringVarP(&c.AppFlag, "app", "a", "", "use a specific app ID or environment")
cmd.PersistentFlags().StringVarP(&c.ConfigDirFlag, "config-dir", "", "", "use a custom path for system config directory")
cmd.PersistentFlags().BoolVarP(&c.DeprecatedDevAppFlag, "local-run", "l", false, "use the local run app created by the `run` command") // deprecated
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.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().StringVarP(&c.RuntimeFlag, "runtime", "r", "", "the project's runtime language:\n deno (default), deno1.1, deno1.x, etc")
cmd.PersistentFlags().BoolVarP(&c.SkipUpdateFlag, "skip-update", "s", false, "skip checking for latest version of CLI")
cmd.PersistentFlags().BoolVarP(&c.SlackDevFlag, "slackdev", "", false, "shorthand for --apihost=https://dev.slack.com")
cmd.PersistentFlags().StringVarP(&c.RuntimeFlag, "runtime", "r", "", "the project's runtime language:\n deno (default), deno1.1, deno1.x, etc")
// TODO - next semver MAJOR can consider a new shorthand flag, right now -t and -T are used by other commands
cmd.PersistentFlags().StringVarP(&c.TeamFlag, "team", "w", "", "select workspace or organization by team name or ID")
cmd.PersistentFlags().StringVarP(&c.TokenFlag, "token", "", "", "set the access token associated with a team")
cmd.PersistentFlags().BoolVarP(&c.DebugEnabled, "verbose", "v", false, "print debug logging and additional info")
cmd.PersistentFlags().StringVarP(&c.DeprecatedWorkspaceFlag, "workspace", "", "", "select workspace or organization by domain name or team ID")

cmd.PersistentFlags().Lookup("apihost").Hidden = true
cmd.PersistentFlags().Lookup("dev").Hidden = true
Expand Down
28 changes: 25 additions & 3 deletions internal/iostreams/forms.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ package iostreams
import (
"context"
"errors"
"fmt"
"slices"
"strings"

huh "charm.land/huh/v2"
"github.com/slackapi/slack-cli/internal/experiment"
Expand All @@ -39,13 +41,24 @@ func newForm(io *IOStreams, field huh.Field) *huh.Form {
} else {
form = form.WithTheme(style.ThemeSurvey())
}
if io != nil && io.config.AccessibleFlag {
form = form.WithAccessible(true)
}
return 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.AccessibleFlag {
if cfg.Placeholder != "" {
title = fmt.Sprintf("%s (default: %s):", strings.TrimSuffix(message, ":"), cfg.Placeholder)
} else if !strings.HasSuffix(message, ":") {
title = message + ":"
}
}
field := huh.NewInput().
Title(message).
Title(title).
Prompt(style.Chevron() + " ").
Placeholder(cfg.Placeholder).
Value(input)
Expand Down Expand Up @@ -100,8 +113,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.AccessibleFlag && len(options) > 0 {
title = fmt.Sprintf("%s (press Enter for 1):", strings.TrimSuffix(msg, ":"))
}

field := huh.NewSelect[string]().
Title(msg).
Title(title).
Description(cfg.Help).
Options(opts...).
Value(selected)
Expand All @@ -125,8 +143,12 @@ func selectForm(io *IOStreams, _ context.Context, msg string, options []string,

// buildPasswordForm constructs an interactive form for password (hidden input) prompts.
func buildPasswordForm(io *IOStreams, message string, cfg PasswordPromptConfig, input *string) *huh.Form {
title := message
if io != nil && io.config.AccessibleFlag && !strings.HasSuffix(message, ":") {
title = message + ":"
}
field := huh.NewInput().
Title(message).
Title(title).
Prompt(style.Chevron() + " ").
EchoMode(huh.EchoModePassword).
Value(input)
Expand Down
72 changes: 72 additions & 0 deletions internal/iostreams/forms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,78 @@ func TestFormsUseSlackTheme(t *testing.T) {
})
}

func TestFormsAccessible(t *testing.T) {
fsMock := slackdeps.NewFsMock()
osMock := slackdeps.NewOsMock()
osMock.AddDefaultMocks()
cfg := config.NewConfig(fsMock, osMock)
cfg.AccessibleFlag = 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("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 1)")
})

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?")
})

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) {
t.Run("forms use plain theme with no-color", func(t *testing.T) {
fsMock := slackdeps.NewFsMock()
Expand Down
Loading