Skip to content
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.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)
Expand Down
2 changes: 2 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -58,6 +59,7 @@ type Config struct {
SlackTestTraceFlag bool
TeamFlag string
TokenFlag string
Accessible bool
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌲 thought: As we're introducing this, should we include the environment variable similar?

ACCESSIBLE

We recommend setting this through an environment variable or configuration option to allow the user to control accessibility.

🔗 https://github.com/charmbracelet/huh?tab=readme-ov-file#accessibility

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 ramble: We might find this adjacent file most useful!

// LoadEnvironmentVariables sets flags based on their environment variable value
func (c *Config) LoadEnvironmentVariables() error {

NoColor bool

// Feature experiments
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.Accessible = 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.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 {
Expand Down
1 change: 1 addition & 0 deletions internal/config/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
17 changes: 15 additions & 2 deletions internal/iostreams/forms.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package iostreams
import (
"context"
"errors"
"fmt"
"slices"

huh "charm.land/huh/v2"
Expand All @@ -39,13 +40,20 @@ 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
}

// 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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
title = fmt.Sprintf("%s (default: %s)", message, cfg.Placeholder)
title = fmt.Sprintf("%s (default: %s):", strings.TrimSuffix(message, ":"), cfg.Placeholder)

🔭 suggestion: This might not be the right syntax but I think formatting input ends best with a colon:

- Name your app: (default: flamboyant-salamander-784)
+ Name your app (default: flamboyant-salamander-784):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 ramble: We don't have a comment on this in the STYLE_GUIDE at this time and we're not consistent on using a : at all or sometimes ? instead. This might be something nice to align over time.

}
field := huh.NewInput().
Title(message).
Title(title).
Prompt(style.Chevron() + " ").
Placeholder(cfg.Placeholder).
Value(input)
Expand Down Expand Up @@ -100,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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
title = fmt.Sprintf("%s (press Enter for 1)", msg)
title = fmt.Sprintf("%s (press Enter for 1):", strings.TrimSuffix(msg, ":"))

🪬 suggestion: Similar to the : suggestion above - I'm more hesitant to this one because some titles might end with "?" but I don't think that formatting is incorrect...

}

field := huh.NewSelect[string]().
Title(msg).
Title(title).
Description(cfg.Help).
Options(opts...).
Value(selected)
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.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("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