Merge pull request #10846 from cli/kw/accessible-prompter-and-disable-spinners-config

`gh config`: add config settings for accessible prompter and disabling spinner
This commit is contained in:
Kynan Ware 2025-04-23 07:40:10 -06:00 committed by GitHub
commit c0f993aca0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 330 additions and 43 deletions

View file

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

View file

@ -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()
}

View file

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

View file

@ -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 {

View file

@ -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"
@ -33,7 +34,7 @@ import (
func TestAccessiblePrompter(t *testing.T) {
t.Run("Select", func(t *testing.T) {
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
p := newTestAccessiblePrompter(t, console)
go func() {
// Wait for prompt to appear
@ -52,7 +53,7 @@ func TestAccessiblePrompter(t *testing.T) {
t.Run("MultiSelect", func(t *testing.T) {
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
p := newTestAccessiblePrompter(t, console)
go func() {
// Wait for prompt to appear
@ -77,7 +78,7 @@ func TestAccessiblePrompter(t *testing.T) {
t.Run("Input", func(t *testing.T) {
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
p := newTestAccessiblePrompter(t, console)
dummyText := "12345abcdefg"
go func() {
@ -97,7 +98,7 @@ func TestAccessiblePrompter(t *testing.T) {
t.Run("Input - blank input returns default value", func(t *testing.T) {
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
p := newTestAccessiblePrompter(t, console)
dummyDefaultValue := "12345abcdefg"
go func() {
@ -117,7 +118,7 @@ func TestAccessiblePrompter(t *testing.T) {
t.Run("Password", func(t *testing.T) {
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
p := newTestAccessiblePrompter(t, console)
dummyPassword := "12345abcdefg"
go func() {
@ -137,7 +138,7 @@ func TestAccessiblePrompter(t *testing.T) {
t.Run("Confirm", func(t *testing.T) {
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
p := newTestAccessiblePrompter(t, console)
go func() {
// Wait for prompt to appear
@ -156,7 +157,7 @@ func TestAccessiblePrompter(t *testing.T) {
t.Run("Confirm - blank input returns default", func(t *testing.T) {
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
p := newTestAccessiblePrompter(t, console)
go func() {
// Wait for prompt to appear
@ -175,7 +176,7 @@ func TestAccessiblePrompter(t *testing.T) {
t.Run("AuthToken", func(t *testing.T) {
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
p := newTestAccessiblePrompter(t, console)
dummyAuthToken := "12345abcdefg"
go func() {
@ -195,7 +196,7 @@ func TestAccessiblePrompter(t *testing.T) {
t.Run("AuthToken - blank input returns error", func(t *testing.T) {
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
p := newTestAccessiblePrompter(t, console)
dummyAuthTokenForAfterFailure := "12345abcdefg"
go func() {
@ -223,7 +224,7 @@ func TestAccessiblePrompter(t *testing.T) {
t.Run("ConfirmDeletion", func(t *testing.T) {
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
p := newTestAccessiblePrompter(t, console)
requiredValue := "test"
go func() {
@ -243,7 +244,7 @@ func TestAccessiblePrompter(t *testing.T) {
t.Run("ConfirmDeletion - bad input", func(t *testing.T) {
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
p := newTestAccessiblePrompter(t, console)
requiredValue := "test"
badInputValue := "garbage"
@ -272,7 +273,7 @@ func TestAccessiblePrompter(t *testing.T) {
t.Run("InputHostname", func(t *testing.T) {
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
p := newTestAccessiblePrompter(t, console)
hostname := "example.com"
go func() {
@ -292,7 +293,7 @@ func TestAccessiblePrompter(t *testing.T) {
t.Run("MarkdownEditor - blank allowed with blank input returns blank", func(t *testing.T) {
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
p := newTestAccessiblePrompter(t, console)
go func() {
// Wait for prompt to appear
@ -311,7 +312,7 @@ func TestAccessiblePrompter(t *testing.T) {
t.Run("MarkdownEditor - blank disallowed with default value returns default value", func(t *testing.T) {
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
p := newTestAccessiblePrompter(t, console)
defaultValue := "12345abcdefg"
go func() {
@ -339,7 +340,7 @@ func TestAccessiblePrompter(t *testing.T) {
t.Run("MarkdownEditor - blank disallowed no default value returns error", func(t *testing.T) {
console := newTestVirtualTerminal(t)
p := newTestAcessiblePrompter(t, console)
p := newTestAccessiblePrompter(t, console)
go func() {
// Wait for prompt to appear
@ -419,21 +420,40 @@ func newTestVirtualTerminal(t *testing.T) *expect.Console {
return console
}
func newTestAcessiblePrompter(t *testing.T, console *expect.Console) prompter.Prompter {
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 newTestAccessiblePrompter(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

View file

@ -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,
}
}

View file

@ -102,6 +102,8 @@ func Test_listRun(t *testing.T) {
browser=brave
color_labels=disabled
accessible_colors=disabled
accessible_prompter=disabled
spinner=enabled
`),
},
}

View file

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

View file

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

View file

@ -23,7 +23,7 @@ func NewClient(httpClient *http.Client, hostname string, ios *iostreams.IOStream
return &Client{
apiClient: apiClient,
io: ios,
prompter: prompter.New("", ios.In, ios.Out, ios.ErrOut),
prompter: prompter.New("", ios),
}
}

View file

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