Update configuration support for accessible colors

- added support for `accessible_colors` configuration setting in `gh config` commandset
- updated default configuration file to contain `accessible_colors: disabled`
- add `GH_ACCESSIBLE_COLORS` env var to `gh environment`
- generated mocks via `go generate ./...` including previously missed prompter changes
This commit is contained in:
Andy Feller 2025-04-17 15:57:49 -04:00
parent 0ef0c42665
commit f61961907e
7 changed files with 169 additions and 96 deletions

View file

@ -14,7 +14,12 @@ import (
ghConfig "github.com/cli/go-gh/v2/pkg/config"
)
// It is important to note that some of these configuration setting keys are used outside of `cli/cli`
// such as `accessible_colors`, `browser`, and `http_unix_socket` used in `cli/go-gh`.
//
// These configuration settings are defined here to avoid `cli/cli` being changed unexpectedly.
const (
accessibleColorsKey = "accessible_colors"
aliasesKey = "aliases"
browserKey = "browser"
colorLabelsKey = "color_labels"
@ -109,6 +114,11 @@ func (c *cfg) Authentication() gh.AuthConfig {
return &AuthConfig{cfg: c.cfg}
}
func (c *cfg) AccessibleColors(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, accessibleColorsKey).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()
@ -540,6 +550,8 @@ http_unix_socket:
browser:
# Whether to display labels using their RGB hex color codes in terminals that support truecolor. Supported values: enabled, disabled
color_labels: disabled
# Whether customizable, 4-bit accessible colors should be used. Supported values: enabled, disabled
accessible_colors: disabled
`
type ConfigOption struct {
@ -619,6 +631,15 @@ var Options = []ConfigOption{
return c.ColorLabels(hostname).Value
},
},
{
Key: accessibleColorsKey,
Description: "whether customizable, 4-bit accessible colors should be used",
DefaultValue: "disabled",
AllowedValues: []string{"enabled", "disabled"},
CurrentValue: func(c gh.Config, hostname string) string {
return c.AccessibleColors(hostname).Value
},
},
}
func HomeDirPath(subdir string) (string, error) {

View file

@ -52,6 +52,9 @@ func NewFromString(cfgStr string) *ghmock.ConfigMock {
},
}
}
mock.AccessibleColorsFunc = func(hostname string) gh.ConfigEntry {
return cfg.AccessibleColors(hostname)
}
mock.BrowserFunc = func(hostname string) gh.ConfigEntry {
return cfg.Browser(hostname)
}

View file

@ -35,6 +35,8 @@ type Config interface {
// Set provides primitive access for setting configuration values, optionally scoped by host.
Set(hostname string, key string, value string)
// AccessibleColors returns the configured accessible_colors setting, optionally scoped by host.
AccessibleColors(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.

View file

@ -19,6 +19,9 @@ var _ gh.Config = &ConfigMock{}
//
// // make and configure a mocked gh.Config
// mockedConfig := &ConfigMock{
// AccessibleColorsFunc: func(hostname string) gh.ConfigEntry {
// panic("mock out the AccessibleColors method")
// },
// AliasesFunc: func() gh.AliasConfig {
// panic("mock out the Aliases method")
// },
@ -74,6 +77,9 @@ var _ gh.Config = &ConfigMock{}
//
// }
type ConfigMock struct {
// AccessibleColorsFunc mocks the AccessibleColors method.
AccessibleColorsFunc func(hostname string) gh.ConfigEntry
// AliasesFunc mocks the Aliases method.
AliasesFunc func() gh.AliasConfig
@ -124,6 +130,11 @@ type ConfigMock struct {
// calls tracks calls to the methods.
calls struct {
// AccessibleColors holds details about calls to the AccessibleColors method.
AccessibleColors []struct {
// Hostname is the hostname argument value.
Hostname string
}
// Aliases holds details about calls to the Aliases method.
Aliases []struct {
}
@ -201,6 +212,7 @@ type ConfigMock struct {
Write []struct {
}
}
lockAccessibleColors sync.RWMutex
lockAliases sync.RWMutex
lockAuthentication sync.RWMutex
lockBrowser sync.RWMutex
@ -219,6 +231,38 @@ type ConfigMock struct {
lockWrite sync.RWMutex
}
// AccessibleColors calls AccessibleColorsFunc.
func (mock *ConfigMock) AccessibleColors(hostname string) gh.ConfigEntry {
if mock.AccessibleColorsFunc == nil {
panic("ConfigMock.AccessibleColorsFunc: method is nil but Config.AccessibleColors was just called")
}
callInfo := struct {
Hostname string
}{
Hostname: hostname,
}
mock.lockAccessibleColors.Lock()
mock.calls.AccessibleColors = append(mock.calls.AccessibleColors, callInfo)
mock.lockAccessibleColors.Unlock()
return mock.AccessibleColorsFunc(hostname)
}
// AccessibleColorsCalls gets all the calls that were made to AccessibleColors.
// Check the length with:
//
// len(mockedConfig.AccessibleColorsCalls())
func (mock *ConfigMock) AccessibleColorsCalls() []struct {
Hostname string
} {
var calls []struct {
Hostname string
}
mock.lockAccessibleColors.RLock()
calls = mock.calls.AccessibleColors
mock.lockAccessibleColors.RUnlock()
return calls
}
// Aliases calls AliasesFunc.
func (mock *ConfigMock) Aliases() gh.AliasConfig {
if mock.AliasesFunc == nil {

View file

@ -20,28 +20,28 @@ var _ Prompter = &PrompterMock{}
// AuthTokenFunc: func() (string, error) {
// panic("mock out the AuthToken method")
// },
// ConfirmFunc: func(s string, b bool) (bool, error) {
// ConfirmFunc: func(prompt string, defaultValue bool) (bool, error) {
// panic("mock out the Confirm method")
// },
// ConfirmDeletionFunc: func(s string) error {
// ConfirmDeletionFunc: func(requiredValue string) error {
// panic("mock out the ConfirmDeletion method")
// },
// InputFunc: func(s1 string, s2 string) (string, error) {
// InputFunc: func(prompt string, defaultValue string) (string, error) {
// panic("mock out the Input method")
// },
// InputHostnameFunc: func() (string, error) {
// panic("mock out the InputHostname method")
// },
// MarkdownEditorFunc: func(s1 string, s2 string, b bool) (string, error) {
// MarkdownEditorFunc: func(prompt string, defaultValue string, blankAllowed bool) (string, error) {
// panic("mock out the MarkdownEditor method")
// },
// MultiSelectFunc: func(prompt string, defaults []string, options []string) ([]int, error) {
// panic("mock out the MultiSelect method")
// },
// PasswordFunc: func(s string) (string, error) {
// PasswordFunc: func(prompt string) (string, error) {
// panic("mock out the Password method")
// },
// SelectFunc: func(s1 string, s2 string, strings []string) (int, error) {
// SelectFunc: func(prompt string, defaultValue string, options []string) (int, error) {
// panic("mock out the Select method")
// },
// }
@ -55,28 +55,28 @@ type PrompterMock struct {
AuthTokenFunc func() (string, error)
// ConfirmFunc mocks the Confirm method.
ConfirmFunc func(s string, b bool) (bool, error)
ConfirmFunc func(prompt string, defaultValue bool) (bool, error)
// ConfirmDeletionFunc mocks the ConfirmDeletion method.
ConfirmDeletionFunc func(s string) error
ConfirmDeletionFunc func(requiredValue string) error
// InputFunc mocks the Input method.
InputFunc func(s1 string, s2 string) (string, error)
InputFunc func(prompt string, defaultValue string) (string, error)
// InputHostnameFunc mocks the InputHostname method.
InputHostnameFunc func() (string, error)
// MarkdownEditorFunc mocks the MarkdownEditor method.
MarkdownEditorFunc func(s1 string, s2 string, b bool) (string, error)
MarkdownEditorFunc func(prompt string, defaultValue string, blankAllowed bool) (string, error)
// MultiSelectFunc mocks the MultiSelect method.
MultiSelectFunc func(prompt string, defaults []string, options []string) ([]int, error)
// PasswordFunc mocks the Password method.
PasswordFunc func(s string) (string, error)
PasswordFunc func(prompt string) (string, error)
// SelectFunc mocks the Select method.
SelectFunc func(s1 string, s2 string, strings []string) (int, error)
SelectFunc func(prompt string, defaultValue string, options []string) (int, error)
// calls tracks calls to the methods.
calls struct {
@ -85,34 +85,34 @@ type PrompterMock struct {
}
// Confirm holds details about calls to the Confirm method.
Confirm []struct {
// S is the s argument value.
S string
// B is the b argument value.
B bool
// Prompt is the prompt argument value.
Prompt string
// DefaultValue is the defaultValue argument value.
DefaultValue bool
}
// ConfirmDeletion holds details about calls to the ConfirmDeletion method.
ConfirmDeletion []struct {
// S is the s argument value.
S string
// RequiredValue is the requiredValue argument value.
RequiredValue string
}
// Input holds details about calls to the Input method.
Input []struct {
// S1 is the s1 argument value.
S1 string
// S2 is the s2 argument value.
S2 string
// Prompt is the prompt argument value.
Prompt string
// DefaultValue is the defaultValue argument value.
DefaultValue string
}
// InputHostname holds details about calls to the InputHostname method.
InputHostname []struct {
}
// MarkdownEditor holds details about calls to the MarkdownEditor method.
MarkdownEditor []struct {
// S1 is the s1 argument value.
S1 string
// S2 is the s2 argument value.
S2 string
// B is the b argument value.
B bool
// Prompt is the prompt argument value.
Prompt string
// DefaultValue is the defaultValue argument value.
DefaultValue string
// BlankAllowed is the blankAllowed argument value.
BlankAllowed bool
}
// MultiSelect holds details about calls to the MultiSelect method.
MultiSelect []struct {
@ -125,17 +125,17 @@ type PrompterMock struct {
}
// Password holds details about calls to the Password method.
Password []struct {
// S is the s argument value.
S string
// Prompt is the prompt argument value.
Prompt string
}
// Select holds details about calls to the Select method.
Select []struct {
// S1 is the s1 argument value.
S1 string
// S2 is the s2 argument value.
S2 string
// Strings is the strings argument value.
Strings []string
// Prompt is the prompt argument value.
Prompt string
// DefaultValue is the defaultValue argument value.
DefaultValue string
// Options is the options argument value.
Options []string
}
}
lockAuthToken sync.RWMutex
@ -177,21 +177,21 @@ func (mock *PrompterMock) AuthTokenCalls() []struct {
}
// Confirm calls ConfirmFunc.
func (mock *PrompterMock) Confirm(s string, b bool) (bool, error) {
func (mock *PrompterMock) Confirm(prompt string, defaultValue bool) (bool, error) {
if mock.ConfirmFunc == nil {
panic("PrompterMock.ConfirmFunc: method is nil but Prompter.Confirm was just called")
}
callInfo := struct {
S string
B bool
Prompt string
DefaultValue bool
}{
S: s,
B: b,
Prompt: prompt,
DefaultValue: defaultValue,
}
mock.lockConfirm.Lock()
mock.calls.Confirm = append(mock.calls.Confirm, callInfo)
mock.lockConfirm.Unlock()
return mock.ConfirmFunc(s, b)
return mock.ConfirmFunc(prompt, defaultValue)
}
// ConfirmCalls gets all the calls that were made to Confirm.
@ -199,12 +199,12 @@ func (mock *PrompterMock) Confirm(s string, b bool) (bool, error) {
//
// len(mockedPrompter.ConfirmCalls())
func (mock *PrompterMock) ConfirmCalls() []struct {
S string
B bool
Prompt string
DefaultValue bool
} {
var calls []struct {
S string
B bool
Prompt string
DefaultValue bool
}
mock.lockConfirm.RLock()
calls = mock.calls.Confirm
@ -213,19 +213,19 @@ func (mock *PrompterMock) ConfirmCalls() []struct {
}
// ConfirmDeletion calls ConfirmDeletionFunc.
func (mock *PrompterMock) ConfirmDeletion(s string) error {
func (mock *PrompterMock) ConfirmDeletion(requiredValue string) error {
if mock.ConfirmDeletionFunc == nil {
panic("PrompterMock.ConfirmDeletionFunc: method is nil but Prompter.ConfirmDeletion was just called")
}
callInfo := struct {
S string
RequiredValue string
}{
S: s,
RequiredValue: requiredValue,
}
mock.lockConfirmDeletion.Lock()
mock.calls.ConfirmDeletion = append(mock.calls.ConfirmDeletion, callInfo)
mock.lockConfirmDeletion.Unlock()
return mock.ConfirmDeletionFunc(s)
return mock.ConfirmDeletionFunc(requiredValue)
}
// ConfirmDeletionCalls gets all the calls that were made to ConfirmDeletion.
@ -233,10 +233,10 @@ func (mock *PrompterMock) ConfirmDeletion(s string) error {
//
// len(mockedPrompter.ConfirmDeletionCalls())
func (mock *PrompterMock) ConfirmDeletionCalls() []struct {
S string
RequiredValue string
} {
var calls []struct {
S string
RequiredValue string
}
mock.lockConfirmDeletion.RLock()
calls = mock.calls.ConfirmDeletion
@ -245,21 +245,21 @@ func (mock *PrompterMock) ConfirmDeletionCalls() []struct {
}
// Input calls InputFunc.
func (mock *PrompterMock) Input(s1 string, s2 string) (string, error) {
func (mock *PrompterMock) Input(prompt string, defaultValue string) (string, error) {
if mock.InputFunc == nil {
panic("PrompterMock.InputFunc: method is nil but Prompter.Input was just called")
}
callInfo := struct {
S1 string
S2 string
Prompt string
DefaultValue string
}{
S1: s1,
S2: s2,
Prompt: prompt,
DefaultValue: defaultValue,
}
mock.lockInput.Lock()
mock.calls.Input = append(mock.calls.Input, callInfo)
mock.lockInput.Unlock()
return mock.InputFunc(s1, s2)
return mock.InputFunc(prompt, defaultValue)
}
// InputCalls gets all the calls that were made to Input.
@ -267,12 +267,12 @@ func (mock *PrompterMock) Input(s1 string, s2 string) (string, error) {
//
// len(mockedPrompter.InputCalls())
func (mock *PrompterMock) InputCalls() []struct {
S1 string
S2 string
Prompt string
DefaultValue string
} {
var calls []struct {
S1 string
S2 string
Prompt string
DefaultValue string
}
mock.lockInput.RLock()
calls = mock.calls.Input
@ -308,23 +308,23 @@ func (mock *PrompterMock) InputHostnameCalls() []struct {
}
// MarkdownEditor calls MarkdownEditorFunc.
func (mock *PrompterMock) MarkdownEditor(s1 string, s2 string, b bool) (string, error) {
func (mock *PrompterMock) MarkdownEditor(prompt string, defaultValue string, blankAllowed bool) (string, error) {
if mock.MarkdownEditorFunc == nil {
panic("PrompterMock.MarkdownEditorFunc: method is nil but Prompter.MarkdownEditor was just called")
}
callInfo := struct {
S1 string
S2 string
B bool
Prompt string
DefaultValue string
BlankAllowed bool
}{
S1: s1,
S2: s2,
B: b,
Prompt: prompt,
DefaultValue: defaultValue,
BlankAllowed: blankAllowed,
}
mock.lockMarkdownEditor.Lock()
mock.calls.MarkdownEditor = append(mock.calls.MarkdownEditor, callInfo)
mock.lockMarkdownEditor.Unlock()
return mock.MarkdownEditorFunc(s1, s2, b)
return mock.MarkdownEditorFunc(prompt, defaultValue, blankAllowed)
}
// MarkdownEditorCalls gets all the calls that were made to MarkdownEditor.
@ -332,14 +332,14 @@ func (mock *PrompterMock) MarkdownEditor(s1 string, s2 string, b bool) (string,
//
// len(mockedPrompter.MarkdownEditorCalls())
func (mock *PrompterMock) MarkdownEditorCalls() []struct {
S1 string
S2 string
B bool
Prompt string
DefaultValue string
BlankAllowed bool
} {
var calls []struct {
S1 string
S2 string
B bool
Prompt string
DefaultValue string
BlankAllowed bool
}
mock.lockMarkdownEditor.RLock()
calls = mock.calls.MarkdownEditor
@ -388,19 +388,19 @@ func (mock *PrompterMock) MultiSelectCalls() []struct {
}
// Password calls PasswordFunc.
func (mock *PrompterMock) Password(s string) (string, error) {
func (mock *PrompterMock) Password(prompt string) (string, error) {
if mock.PasswordFunc == nil {
panic("PrompterMock.PasswordFunc: method is nil but Prompter.Password was just called")
}
callInfo := struct {
S string
Prompt string
}{
S: s,
Prompt: prompt,
}
mock.lockPassword.Lock()
mock.calls.Password = append(mock.calls.Password, callInfo)
mock.lockPassword.Unlock()
return mock.PasswordFunc(s)
return mock.PasswordFunc(prompt)
}
// PasswordCalls gets all the calls that were made to Password.
@ -408,10 +408,10 @@ func (mock *PrompterMock) Password(s string) (string, error) {
//
// len(mockedPrompter.PasswordCalls())
func (mock *PrompterMock) PasswordCalls() []struct {
S string
Prompt string
} {
var calls []struct {
S string
Prompt string
}
mock.lockPassword.RLock()
calls = mock.calls.Password
@ -420,23 +420,23 @@ func (mock *PrompterMock) PasswordCalls() []struct {
}
// Select calls SelectFunc.
func (mock *PrompterMock) Select(s1 string, s2 string, strings []string) (int, error) {
func (mock *PrompterMock) Select(prompt string, defaultValue string, options []string) (int, error) {
if mock.SelectFunc == nil {
panic("PrompterMock.SelectFunc: method is nil but Prompter.Select was just called")
}
callInfo := struct {
S1 string
S2 string
Strings []string
Prompt string
DefaultValue string
Options []string
}{
S1: s1,
S2: s2,
Strings: strings,
Prompt: prompt,
DefaultValue: defaultValue,
Options: options,
}
mock.lockSelect.Lock()
mock.calls.Select = append(mock.calls.Select, callInfo)
mock.lockSelect.Unlock()
return mock.SelectFunc(s1, s2, strings)
return mock.SelectFunc(prompt, defaultValue, options)
}
// SelectCalls gets all the calls that were made to Select.
@ -444,14 +444,14 @@ func (mock *PrompterMock) Select(s1 string, s2 string, strings []string) (int, e
//
// len(mockedPrompter.SelectCalls())
func (mock *PrompterMock) SelectCalls() []struct {
S1 string
S2 string
Strings []string
Prompt string
DefaultValue string
Options []string
} {
var calls []struct {
S1 string
S2 string
Strings []string
Prompt string
DefaultValue string
Options []string
}
mock.lockSelect.RLock()
calls = mock.calls.Select

View file

@ -101,6 +101,7 @@ func Test_listRun(t *testing.T) {
http_unix_socket=
browser=brave
color_labels=disabled
accessible_colors=disabled
`),
},
}

View file

@ -84,6 +84,8 @@ var HelpTopics = []helpTopic{
%[1]sGH_COLOR_LABELS%[1]s: set to any value to display labels using their RGB hex color codes in terminals that
support truecolor.
%[1]sGH_ACCESSIBLE_COLORS%[1]s (preview): set to a truthy value to use customizable, 4-bit accessible colors.
%[1]sGH_FORCE_TTY%[1]s: set to any value to force terminal-style output even when the output is
redirected. When the value is a number, it is interpreted as the number of columns
available in the viewport. When the value is a percentage, it will be applied against