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.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
AccessibleFlag bool
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 @@
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

Check failure on line 33 in internal/config/dotenv.go

View workflow job for this annotation

GitHub Actions / Lints and Tests

c.Accessible undefined (type *Config has no field or method Accessible)
}

// 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 @@
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)

Check failure on line 174 in internal/config/dotenv_test.go

View workflow job for this annotation

GitHub Actions / Lints and Tests

cfg.Accessible undefined (type *Config has no field or method 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)

Check failure on line 181 in internal/config/dotenv_test.go

View workflow job for this annotation

GitHub Actions / Lints and Tests

cfg.Accessible undefined (type *Config has no field or method 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)

Check failure on line 188 in internal/config/dotenv_test.go

View workflow job for this annotation

GitHub Actions / Lints and Tests

cfg.Accessible undefined (type *Config has no field or method 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)

Check failure on line 195 in internal/config/dotenv_test.go

View workflow job for this annotation

GitHub Actions / Lints and Tests

cfg.Accessible undefined (type *Config has no field or method Accessible)
},
},
"empty ACCESSIBLE should set Accessible to false": {
envName: "ACCESSIBLE",
envValue: "",
assertOnConfig: func(t *testing.T, cfg *Config) {
assert.Equal(t, false, cfg.Accessible)

Check failure on line 202 in internal/config/dotenv_test.go

View workflow job for this annotation

GitHub Actions / Lints and Tests

cfg.Accessible undefined (type *Config has no field or method Accessible) (typecheck)
},
},
}

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 @@
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")

Check failure on line 40 in internal/config/flags.go

View workflow job for this annotation

GitHub Actions / Lints and Tests

c.Accessible undefined (type *Config has no field or method Accessible)
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
18 changes: 16 additions & 2 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,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):", strings.TrimSuffix(message, ":"), cfg.Placeholder)
}
Comment on lines +53 to +55
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.

📸 suggestion: Let's add a missing ":" to inputs without placeholder too:

With interactive prompts - Unsure if this requires a change but might be nice? If it doesn't feel right we can hold off on update here!

Image

Without interactive prompts - Feels more meaningful to have a split between output and input

Image

📣 I suggest the form following:

Variable name:

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.

🔬 note: Similar comment might be applied to password forms and others?

field := huh.NewInput().
Title(message).
Title(title).
Prompt(style.Chevron() + " ").
Placeholder(cfg.Placeholder).
Value(input)
Expand Down Expand Up @@ -100,8 +109,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):", strings.TrimSuffix(msg, ":"))
}

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