Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 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 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
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.AccessibleFlag, "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
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