From 138bccd4377d09fe778f9847a4aac3c31121cdca Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Mon, 21 Apr 2025 16:15:41 -0600 Subject: [PATCH] feat(config): add accessible prompter and spinner --- internal/config/config.go | 34 ++++++ internal/config/stub.go | 6 + internal/gh/gh.go | 4 + internal/gh/mock/config.go | 88 ++++++++++++++ internal/prompter/accessible_prompter_test.go | 34 ++++-- internal/prompter/prompter.go | 24 ++-- pkg/cmd/config/list/list_test.go | 2 + pkg/cmd/factory/default.go | 20 ++- pkg/cmd/factory/default_test.go | 114 ++++++++++++++++++ pkg/cmd/project/shared/queries/queries.go | 2 +- pkg/iostreams/iostreams.go | 13 +- 11 files changed, 314 insertions(+), 27 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index c78dac15c..003a0ca17 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,6 +18,7 @@ import ( // they are defined here to avoid `cli/cli` being changed unexpectedly. const ( accessibleColorsKey = "accessible_colors" // used by cli/go-gh to enable the use of customizable, accessible 4-bit colors. + accessiblePrompterKey = "accessible_prompter" aliasesKey = "aliases" browserKey = "browser" // used by cli/go-gh to open URLs in web browsers colorLabelsKey = "color_labels" @@ -29,6 +30,7 @@ const ( pagerKey = "pager" promptKey = "prompt" preferEditorPromptKey = "prefer_editor_prompt" + spinnerKey = "spinner" userKey = "user" usersKey = "users" versionKey = "version" @@ -117,6 +119,11 @@ func (c *cfg) AccessibleColors(hostname string) gh.ConfigEntry { return c.GetOrDefault(hostname, accessibleColorsKey).Unwrap() } +func (c *cfg) AccessiblePrompter(hostname string) gh.ConfigEntry { + // Intentionally panic if there is no user provided value or default value (which would be a programmer error) + return c.GetOrDefault(hostname, accessiblePrompterKey).Unwrap() +} + func (c *cfg) Browser(hostname string) gh.ConfigEntry { // Intentionally panic if there is no user provided value or default value (which would be a programmer error) return c.GetOrDefault(hostname, browserKey).Unwrap() @@ -157,6 +164,11 @@ func (c *cfg) PreferEditorPrompt(hostname string) gh.ConfigEntry { return c.GetOrDefault(hostname, preferEditorPromptKey).Unwrap() } +func (c *cfg) Spinner(hostname string) gh.ConfigEntry { + // Intentionally panic if there is no user provided value or default value (which would be a programmer error) + return c.GetOrDefault(hostname, spinnerKey).Unwrap() +} + func (c *cfg) Version() o.Option[string] { return c.get("", versionKey) } @@ -550,6 +562,10 @@ browser: color_labels: disabled # Whether customizable, 4-bit accessible colors should be used. Supported values: enabled, disabled accessible_colors: disabled +# Whether an accessible prompter should be used. Supported values: enabled, disabled +accessible_prompter: disabled +# Whether to use a animated spinner as a progress indicator. If disabled, a textual progress indicator is used instead. Supported values: enabled, disabled +spinner: enabled ` type ConfigOption struct { @@ -638,6 +654,24 @@ var Options = []ConfigOption{ return c.AccessibleColors(hostname).Value }, }, + { + Key: accessiblePrompterKey, + Description: "whether an accessible prompter should be used", + DefaultValue: "disabled", + AllowedValues: []string{"enabled", "disabled"}, + CurrentValue: func(c gh.Config, hostname string) string { + return c.AccessiblePrompter(hostname).Value + }, + }, + { + Key: spinnerKey, + Description: "whether to use a animated spinner as a progress indicator", + DefaultValue: "enabled", + AllowedValues: []string{"enabled", "disabled"}, + CurrentValue: func(c gh.Config, hostname string) string { + return c.Spinner(hostname).Value + }, + }, } func HomeDirPath(subdir string) (string, error) { diff --git a/internal/config/stub.go b/internal/config/stub.go index 8b9f14290..ea60254db 100644 --- a/internal/config/stub.go +++ b/internal/config/stub.go @@ -55,6 +55,9 @@ func NewFromString(cfgStr string) *ghmock.ConfigMock { mock.AccessibleColorsFunc = func(hostname string) gh.ConfigEntry { return cfg.AccessibleColors(hostname) } + mock.AccessiblePrompterFunc = func(hostname string) gh.ConfigEntry { + return cfg.AccessiblePrompter(hostname) + } mock.BrowserFunc = func(hostname string) gh.ConfigEntry { return cfg.Browser(hostname) } @@ -79,6 +82,9 @@ func NewFromString(cfgStr string) *ghmock.ConfigMock { mock.PreferEditorPromptFunc = func(hostname string) gh.ConfigEntry { return cfg.PreferEditorPrompt(hostname) } + mock.SpinnerFunc = func(hostname string) gh.ConfigEntry { + return cfg.Spinner(hostname) + } mock.VersionFunc = func() o.Option[string] { return cfg.Version() } diff --git a/internal/gh/gh.go b/internal/gh/gh.go index 8f3e3cd5b..aa90a5268 100644 --- a/internal/gh/gh.go +++ b/internal/gh/gh.go @@ -37,6 +37,8 @@ type Config interface { // AccessibleColors returns the configured accessible_colors setting, optionally scoped by host. AccessibleColors(hostname string) ConfigEntry + // AccessiblePrompter returns the configured accessible_prompter setting, optionally scoped by host. + AccessiblePrompter(hostname string) ConfigEntry // Browser returns the configured browser, optionally scoped by host. Browser(hostname string) ConfigEntry // ColorLabels returns the configured color_label setting, optionally scoped by host. @@ -53,6 +55,8 @@ type Config interface { Prompt(hostname string) ConfigEntry // PreferEditorPrompt returns the configured editor-based prompt, optionally scoped by host. PreferEditorPrompt(hostname string) ConfigEntry + // Spinner returns the configured spinner setting, optionally scoped by host. + Spinner(hostname string) ConfigEntry // Aliases provides persistent storage and modification of command aliases. Aliases() AliasConfig diff --git a/internal/gh/mock/config.go b/internal/gh/mock/config.go index 600eea5c1..9f3f80799 100644 --- a/internal/gh/mock/config.go +++ b/internal/gh/mock/config.go @@ -22,6 +22,9 @@ var _ gh.Config = &ConfigMock{} // AccessibleColorsFunc: func(hostname string) gh.ConfigEntry { // panic("mock out the AccessibleColors method") // }, +// AccessiblePrompterFunc: func(hostname string) gh.ConfigEntry { +// panic("mock out the AccessiblePrompter method") +// }, // AliasesFunc: func() gh.AliasConfig { // panic("mock out the Aliases method") // }, @@ -64,6 +67,9 @@ var _ gh.Config = &ConfigMock{} // SetFunc: func(hostname string, key string, value string) { // panic("mock out the Set method") // }, +// SpinnerFunc: func(hostname string) gh.ConfigEntry { +// panic("mock out the Spinner method") +// }, // VersionFunc: func() o.Option[string] { // panic("mock out the Version method") // }, @@ -80,6 +86,9 @@ type ConfigMock struct { // AccessibleColorsFunc mocks the AccessibleColors method. AccessibleColorsFunc func(hostname string) gh.ConfigEntry + // AccessiblePrompterFunc mocks the AccessiblePrompter method. + AccessiblePrompterFunc func(hostname string) gh.ConfigEntry + // AliasesFunc mocks the Aliases method. AliasesFunc func() gh.AliasConfig @@ -122,6 +131,9 @@ type ConfigMock struct { // SetFunc mocks the Set method. SetFunc func(hostname string, key string, value string) + // SpinnerFunc mocks the Spinner method. + SpinnerFunc func(hostname string) gh.ConfigEntry + // VersionFunc mocks the Version method. VersionFunc func() o.Option[string] @@ -135,6 +147,11 @@ type ConfigMock struct { // Hostname is the hostname argument value. Hostname string } + // AccessiblePrompter holds details about calls to the AccessiblePrompter method. + AccessiblePrompter []struct { + // Hostname is the hostname argument value. + Hostname string + } // Aliases holds details about calls to the Aliases method. Aliases []struct { } @@ -205,6 +222,11 @@ type ConfigMock struct { // Value is the value argument value. Value string } + // Spinner holds details about calls to the Spinner method. + Spinner []struct { + // Hostname is the hostname argument value. + Hostname string + } // Version holds details about calls to the Version method. Version []struct { } @@ -213,6 +235,7 @@ type ConfigMock struct { } } lockAccessibleColors sync.RWMutex + lockAccessiblePrompter sync.RWMutex lockAliases sync.RWMutex lockAuthentication sync.RWMutex lockBrowser sync.RWMutex @@ -227,6 +250,7 @@ type ConfigMock struct { lockPreferEditorPrompt sync.RWMutex lockPrompt sync.RWMutex lockSet sync.RWMutex + lockSpinner sync.RWMutex lockVersion sync.RWMutex lockWrite sync.RWMutex } @@ -263,6 +287,38 @@ func (mock *ConfigMock) AccessibleColorsCalls() []struct { return calls } +// AccessiblePrompter calls AccessiblePrompterFunc. +func (mock *ConfigMock) AccessiblePrompter(hostname string) gh.ConfigEntry { + if mock.AccessiblePrompterFunc == nil { + panic("ConfigMock.AccessiblePrompterFunc: method is nil but Config.AccessiblePrompter was just called") + } + callInfo := struct { + Hostname string + }{ + Hostname: hostname, + } + mock.lockAccessiblePrompter.Lock() + mock.calls.AccessiblePrompter = append(mock.calls.AccessiblePrompter, callInfo) + mock.lockAccessiblePrompter.Unlock() + return mock.AccessiblePrompterFunc(hostname) +} + +// AccessiblePrompterCalls gets all the calls that were made to AccessiblePrompter. +// Check the length with: +// +// len(mockedConfig.AccessiblePrompterCalls()) +func (mock *ConfigMock) AccessiblePrompterCalls() []struct { + Hostname string +} { + var calls []struct { + Hostname string + } + mock.lockAccessiblePrompter.RLock() + calls = mock.calls.AccessiblePrompter + mock.lockAccessiblePrompter.RUnlock() + return calls +} + // Aliases calls AliasesFunc. func (mock *ConfigMock) Aliases() gh.AliasConfig { if mock.AliasesFunc == nil { @@ -708,6 +764,38 @@ func (mock *ConfigMock) SetCalls() []struct { return calls } +// Spinner calls SpinnerFunc. +func (mock *ConfigMock) Spinner(hostname string) gh.ConfigEntry { + if mock.SpinnerFunc == nil { + panic("ConfigMock.SpinnerFunc: method is nil but Config.Spinner was just called") + } + callInfo := struct { + Hostname string + }{ + Hostname: hostname, + } + mock.lockSpinner.Lock() + mock.calls.Spinner = append(mock.calls.Spinner, callInfo) + mock.lockSpinner.Unlock() + return mock.SpinnerFunc(hostname) +} + +// SpinnerCalls gets all the calls that were made to Spinner. +// Check the length with: +// +// len(mockedConfig.SpinnerCalls()) +func (mock *ConfigMock) SpinnerCalls() []struct { + Hostname string +} { + var calls []struct { + Hostname string + } + mock.lockSpinner.RLock() + calls = mock.calls.Spinner + mock.lockSpinner.RUnlock() + return calls +} + // Version calls VersionFunc. func (mock *ConfigMock) Version() o.Option[string] { if mock.VersionFunc == nil { diff --git a/internal/prompter/accessible_prompter_test.go b/internal/prompter/accessible_prompter_test.go index 56096972d..8ffad10e2 100644 --- a/internal/prompter/accessible_prompter_test.go +++ b/internal/prompter/accessible_prompter_test.go @@ -11,6 +11,7 @@ import ( "github.com/Netflix/go-expect" "github.com/cli/cli/v2/internal/prompter" + "github.com/cli/cli/v2/pkg/iostreams" "github.com/creack/pty" "github.com/hinshun/vt10x" "github.com/stretchr/testify/assert" @@ -419,21 +420,40 @@ func newTestVirtualTerminal(t *testing.T) *expect.Console { return console } +func newTestVirtualTerminalIOStreams(t *testing.T, console *expect.Console) *iostreams.IOStreams { + t.Helper() + io := &iostreams.IOStreams{ + In: console.Tty(), + Out: console.Tty(), + ErrOut: console.Tty(), + } + io.SetStdinTTY(false) + io.SetStdoutTTY(false) + io.SetStderrTTY(false) + return io +} + +// `echo` is chosen as the editor command because it immediately returns +// a success exit code, returns an empty string, doesn't require any user input, +// and since this file is only built on Linux, it is near guaranteed to be available. +var editorCmd = "echo" + func newTestAcessiblePrompter(t *testing.T, console *expect.Console) prompter.Prompter { t.Helper() - t.Setenv("GH_ACCESSIBLE_PROMPTER", "true") - // `echo`` is chose as the editor command because it immediately returns - // a success exit code, returns an empty string, doesn't require any user input, - // and since this file is only built on Linux, it is near guaranteed to be available. - return prompter.New("echo", console.Tty(), console.Tty(), console.Tty()) + io := newTestVirtualTerminalIOStreams(t, console) + io.SetAccessiblePrompterEnabled(true) + + return prompter.New(editorCmd, io) } func newTestSurveyPrompter(t *testing.T, console *expect.Console) prompter.Prompter { t.Helper() - t.Setenv("GH_ACCESSIBLE_PROMPTER", "false") - return prompter.New("echo", console.Tty(), console.Tty(), console.Tty()) + io := newTestVirtualTerminalIOStreams(t, console) + io.SetAccessiblePrompterEnabled(false) + + return prompter.New(editorCmd, io) } // failOnExpectError adds an observer that will fail the test in a standardised way diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 6ef61cf15..2a4328366 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -2,13 +2,12 @@ package prompter import ( "fmt" - "os" - "slices" "strings" "github.com/AlecAivazis/survey/v2" "github.com/charmbracelet/huh" "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/pkg/surveyext" ghPrompter "github.com/cli/go-gh/v2/pkg/prompter" ) @@ -43,24 +42,21 @@ type Prompter interface { MarkdownEditor(prompt string, defaultValue string, blankAllowed bool) (string, error) } -func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWriter, stderr ghPrompter.FileWriter) Prompter { - accessiblePrompterValue, accessiblePrompterIsSet := os.LookupEnv("GH_ACCESSIBLE_PROMPTER") - falseyValues := []string{"false", "0", "no", ""} - - if accessiblePrompterIsSet && !slices.Contains(falseyValues, accessiblePrompterValue) { +func New(editorCmd string, io *iostreams.IOStreams) Prompter { + if io.AccessiblePrompterEnabled() { return &accessiblePrompter{ - stdin: stdin, - stdout: stdout, - stderr: stderr, + stdin: io.In, + stdout: io.Out, + stderr: io.ErrOut, editorCmd: editorCmd, } } return &surveyPrompter{ - prompter: ghPrompter.New(stdin, stdout, stderr), - stdin: stdin, - stdout: stdout, - stderr: stderr, + prompter: ghPrompter.New(io.In, io.Out, io.ErrOut), + stdin: io.In, + stdout: io.Out, + stderr: io.ErrOut, editorCmd: editorCmd, } } diff --git a/pkg/cmd/config/list/list_test.go b/pkg/cmd/config/list/list_test.go index 2a1dd72ee..27260e857 100644 --- a/pkg/cmd/config/list/list_test.go +++ b/pkg/cmd/config/list/list_test.go @@ -102,6 +102,8 @@ func Test_listRun(t *testing.T) { browser=brave color_labels=disabled accessible_colors=disabled + accessible_prompter=disabled + spinner=enabled `), }, } diff --git a/pkg/cmd/factory/default.go b/pkg/cmd/factory/default.go index 7a45efeb4..52837b252 100644 --- a/pkg/cmd/factory/default.go +++ b/pkg/cmd/factory/default.go @@ -227,7 +227,7 @@ func newBrowser(f *cmdutil.Factory) browser.Browser { func newPrompter(f *cmdutil.Factory) prompter.Prompter { editor, _ := cmdutil.DetermineEditor(f.Config) io := f.IOStreams - return prompter.New(editor, io.In, io.Out, io.ErrOut) + return prompter.New(editor, io) } func configFunc() func() (gh.Config, error) { @@ -284,9 +284,23 @@ func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams { io.SetNeverPrompt(true) } - ghSpinnerDisabledValue, ghSpinnerDisabledIsSet := os.LookupEnv("GH_SPINNER_DISABLED") falseyValues := []string{"false", "0", "no", ""} - if ghSpinnerDisabledIsSet && !slices.Contains(falseyValues, ghSpinnerDisabledValue) { + + accessiblePrompterValue, accessiblePrompterIsSet := os.LookupEnv("GH_ACCESSIBLE_PROMPTER") + if accessiblePrompterIsSet { + if !slices.Contains(falseyValues, accessiblePrompterValue) { + io.SetAccessiblePrompterEnabled(true) + } + } else if prompt := cfg.AccessiblePrompter(""); prompt.Value == "enabled" { + io.SetAccessiblePrompterEnabled(true) + } + + ghSpinnerDisabledValue, ghSpinnerDisabledIsSet := os.LookupEnv("GH_SPINNER_DISABLED") + if ghSpinnerDisabledIsSet { + if !slices.Contains(falseyValues, ghSpinnerDisabledValue) { + io.SetSpinnerDisabled(true) + } + } else if spinnerDisabled := cfg.Spinner(""); spinnerDisabled.Value == "disabled" { io.SetSpinnerDisabled(true) } diff --git a/pkg/cmd/factory/default_test.go b/pkg/cmd/factory/default_test.go index 5036a1dc1..d7bfe39fd 100644 --- a/pkg/cmd/factory/default_test.go +++ b/pkg/cmd/factory/default_test.go @@ -435,6 +435,7 @@ func Test_ioStreams_prompt(t *testing.T) { func Test_ioStreams_spinnerDisabled(t *testing.T) { tests := []struct { name string + config gh.Config spinnerDisabled bool env map[string]string }{ @@ -442,6 +443,16 @@ func Test_ioStreams_spinnerDisabled(t *testing.T) { name: "default config", spinnerDisabled: false, }, + { + name: "config with spinner disabled", + config: disableSpinnersConfig(), + spinnerDisabled: true, + }, + { + name: "config with spinner enabled", + config: enableSpinnersConfig(), + spinnerDisabled: false, + }, { name: "spinner disabled via GH_SPINNER_DISABLED env var = 0", env: map[string]string{"GH_SPINNER_DISABLED": "0"}, @@ -467,6 +478,18 @@ func Test_ioStreams_spinnerDisabled(t *testing.T) { env: map[string]string{"GH_SPINNER_DISABLED": "true"}, spinnerDisabled: true, }, + { + name: "config enabled but env disabled, respects env", + config: enableSpinnersConfig(), + env: map[string]string{"GH_SPINNER_DISABLED": "true"}, + spinnerDisabled: true, + }, + { + name: "config disabled but env enabled, respects env", + config: disableSpinnersConfig(), + env: map[string]string{"GH_SPINNER_DISABLED": "false"}, + spinnerDisabled: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -474,12 +497,87 @@ func Test_ioStreams_spinnerDisabled(t *testing.T) { t.Setenv(k, v) } f := New("1") + f.Config = func() (gh.Config, error) { + if tt.config == nil { + return config.NewBlankConfig(), nil + } else { + return tt.config, nil + } + } io := ioStreams(f) assert.Equal(t, tt.spinnerDisabled, io.GetSpinnerDisabled()) }) } } +func Test_ioStreams_accessiblePrompterEnabled(t *testing.T) { + tests := []struct { + name string + config gh.Config + accessiblePrompterEnabled bool + env map[string]string + }{ + { + name: "default config", + accessiblePrompterEnabled: false, + }, + { + name: "config with accessible prompter enabled", + config: enableAccessiblePrompterConfig(), + accessiblePrompterEnabled: true, + }, + { + name: "config with accessible prompter disabled", + config: disableAccessiblePrompterConfig(), + accessiblePrompterEnabled: false, + }, + { + name: "accessible prompter enabled via GH_ACCESSIBLE_PROMPTER env var = 1", + env: map[string]string{"GH_ACCESSIBLE_PROMPTER": "1"}, + accessiblePrompterEnabled: true, + }, + { + name: "accessible prompter enabled via GH_ACCESSIBLE_PROMPTER env var = true", + env: map[string]string{"GH_ACCESSIBLE_PROMPTER": "true"}, + accessiblePrompterEnabled: true, + }, + { + name: "accessible prompter disabled via GH_ACCESSIBLE_PROMPTER env var = 0", + env: map[string]string{"GH_ACCESSIBLE_PROMPTER": "0"}, + accessiblePrompterEnabled: false, + }, + { + name: "config disabled but env enabled, respects env", + config: disableAccessiblePrompterConfig(), + env: map[string]string{"GH_ACCESSIBLE_PROMPTER": "true"}, + accessiblePrompterEnabled: true, + }, + { + name: "config enabled but env disabled, respects env", + config: enableAccessiblePrompterConfig(), + env: map[string]string{"GH_ACCESSIBLE_PROMPTER": "false"}, + accessiblePrompterEnabled: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for k, v := range tt.env { + t.Setenv(k, v) + } + f := New("1") + f.Config = func() (gh.Config, error) { + if tt.config == nil { + return config.NewBlankConfig(), nil + } else { + return tt.config, nil + } + } + io := ioStreams(f) + assert.Equal(t, tt.accessiblePrompterEnabled, io.AccessiblePrompterEnabled()) + }) + } +} + func Test_ioStreams_colorLabels(t *testing.T) { tests := []struct { name string @@ -664,6 +762,22 @@ func disablePromptConfig() gh.Config { return config.NewFromString("prompt: disabled") } +func enableAccessiblePrompterConfig() gh.Config { + return config.NewFromString("accessible_prompter: enabled") +} + +func disableAccessiblePrompterConfig() gh.Config { + return config.NewFromString("accessible_prompter: disabled") +} + +func disableSpinnersConfig() gh.Config { + return config.NewFromString("spinner: disabled") +} + +func enableSpinnersConfig() gh.Config { + return config.NewFromString("spinner: enabled") +} + func disableColorLabelsConfig() gh.Config { return config.NewFromString("color_labels: disabled") } diff --git a/pkg/cmd/project/shared/queries/queries.go b/pkg/cmd/project/shared/queries/queries.go index 3e63465dd..87644a4ce 100644 --- a/pkg/cmd/project/shared/queries/queries.go +++ b/pkg/cmd/project/shared/queries/queries.go @@ -25,7 +25,7 @@ func NewClient(httpClient *http.Client, hostname string, ios *iostreams.IOStream return &Client{ apiClient: apiClient, spinner: ios.IsStdoutTTY() && ios.IsStderrTTY(), - prompter: prompter.New("", ios.In, ios.Out, ios.ErrOut), + prompter: prompter.New("", ios), } } diff --git a/pkg/iostreams/iostreams.go b/pkg/iostreams/iostreams.go index ba2cc6b50..22f966ac8 100644 --- a/pkg/iostreams/iostreams.go +++ b/pkg/iostreams/iostreams.go @@ -58,6 +58,7 @@ type IOStreams struct { progressIndicatorEnabled bool progressIndicator *spinner.Spinner progressIndicatorMu sync.Mutex + spinnerDisabled bool alternateScreenBufferEnabled bool alternateScreenBufferActive bool @@ -78,8 +79,8 @@ type IOStreams struct { pagerCommand string pagerProcess *os.Process - neverPrompt bool - spinnerDisabled bool + neverPrompt bool + accessiblePrompterEnabled bool TempFileOverride *os.File } @@ -457,6 +458,14 @@ func (s *IOStreams) AccessibleColorsEnabled() bool { return s.accessibleColorsEnabled } +func (s *IOStreams) SetAccessiblePrompterEnabled(enabled bool) { + s.accessiblePrompterEnabled = enabled +} + +func (s *IOStreams) AccessiblePrompterEnabled() bool { + return s.accessiblePrompterEnabled +} + func System() *IOStreams { terminal := ghTerm.FromEnv()