Merge branch 'trunk' into refactor-sigstore-verifier-logic

This commit is contained in:
Meredith Lancaster 2025-04-08 16:00:40 -06:00 committed by GitHub
commit a3bfb158e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 460 additions and 222 deletions

View file

@ -17,6 +17,7 @@ import (
const (
aliasesKey = "aliases"
browserKey = "browser"
colorLabelsKey = "color_labels"
editorKey = "editor"
gitProtocolKey = "git_protocol"
hostsKey = "hosts"
@ -113,6 +114,11 @@ func (c *cfg) Browser(hostname string) gh.ConfigEntry {
return c.GetOrDefault(hostname, browserKey).Unwrap()
}
func (c *cfg) ColorLabels(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, colorLabelsKey).Unwrap()
}
func (c *cfg) Editor(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, editorKey).Unwrap()
@ -532,6 +538,8 @@ aliases:
http_unix_socket:
# What web browser gh should use when opening URLs. If blank, will refer to environment.
browser:
# Whether to display labels using their RGB hex color codes in terminals that support truecolor. Supported values: enabled, disabled
color_labels: disabled
`
type ConfigOption struct {
@ -602,6 +610,15 @@ var Options = []ConfigOption{
return c.Browser(hostname).Value
},
},
{
Key: colorLabelsKey,
Description: "whether to display labels using their RGB hex color codes in terminals that support truecolor",
DefaultValue: "disabled",
AllowedValues: []string{"enabled", "disabled"},
CurrentValue: func(c gh.Config, hostname string) string {
return c.ColorLabels(hostname).Value
},
},
}
func HomeDirPath(subdir string) (string, error) {

View file

@ -32,6 +32,7 @@ func TestNewConfigProvidesFallback(t *testing.T) {
requireKeyWithValue(t, spiedCfg, []string{aliasesKey, "co"}, "pr checkout")
requireKeyWithValue(t, spiedCfg, []string{httpUnixSocketKey}, "")
requireKeyWithValue(t, spiedCfg, []string{browserKey}, "")
requireKeyWithValue(t, spiedCfg, []string{colorLabelsKey}, "disabled")
}
func TestGetOrDefaultApplicationDefaults(t *testing.T) {
@ -137,6 +138,7 @@ func TestFallbackConfig(t *testing.T) {
requireKeyWithValue(t, cfg, []string{aliasesKey, "co"}, "pr checkout")
requireKeyWithValue(t, cfg, []string{httpUnixSocketKey}, "")
requireKeyWithValue(t, cfg, []string{browserKey}, "")
requireKeyWithValue(t, cfg, []string{colorLabelsKey}, "disabled")
requireNoKey(t, cfg, []string{"unknown"})
}

View file

@ -55,6 +55,9 @@ func NewFromString(cfgStr string) *ghmock.ConfigMock {
mock.BrowserFunc = func(hostname string) gh.ConfigEntry {
return cfg.Browser(hostname)
}
mock.ColorLabelsFunc = func(hostname string) gh.ConfigEntry {
return cfg.ColorLabels(hostname)
}
mock.EditorFunc = func(hostname string) gh.ConfigEntry {
return cfg.Editor(hostname)
}

View file

@ -37,6 +37,8 @@ type Config interface {
// Browser returns the configured browser, optionally scoped by host.
Browser(hostname string) ConfigEntry
// ColorLabels returns the configured color_label setting, optionally scoped by host.
ColorLabels(hostname string) ConfigEntry
// Editor returns the configured editor, optionally scoped by host.
Editor(hostname string) ConfigEntry
// GitProtocol returns the configured git protocol, optionally scoped by host.

View file

@ -31,6 +31,9 @@ var _ gh.Config = &ConfigMock{}
// CacheDirFunc: func() string {
// panic("mock out the CacheDir method")
// },
// ColorLabelsFunc: func(hostname string) gh.ConfigEntry {
// panic("mock out the ColorLabels method")
// },
// EditorFunc: func(hostname string) gh.ConfigEntry {
// panic("mock out the Editor method")
// },
@ -83,6 +86,9 @@ type ConfigMock struct {
// CacheDirFunc mocks the CacheDir method.
CacheDirFunc func() string
// ColorLabelsFunc mocks the ColorLabels method.
ColorLabelsFunc func(hostname string) gh.ConfigEntry
// EditorFunc mocks the Editor method.
EditorFunc func(hostname string) gh.ConfigEntry
@ -132,6 +138,11 @@ type ConfigMock struct {
// CacheDir holds details about calls to the CacheDir method.
CacheDir []struct {
}
// ColorLabels holds details about calls to the ColorLabels method.
ColorLabels []struct {
// Hostname is the hostname argument value.
Hostname string
}
// Editor holds details about calls to the Editor method.
Editor []struct {
// Hostname is the hostname argument value.
@ -194,6 +205,7 @@ type ConfigMock struct {
lockAuthentication sync.RWMutex
lockBrowser sync.RWMutex
lockCacheDir sync.RWMutex
lockColorLabels sync.RWMutex
lockEditor sync.RWMutex
lockGetOrDefault sync.RWMutex
lockGitProtocol sync.RWMutex
@ -320,6 +332,38 @@ func (mock *ConfigMock) CacheDirCalls() []struct {
return calls
}
// ColorLabels calls ColorLabelsFunc.
func (mock *ConfigMock) ColorLabels(hostname string) gh.ConfigEntry {
if mock.ColorLabelsFunc == nil {
panic("ConfigMock.ColorLabelsFunc: method is nil but Config.ColorLabels was just called")
}
callInfo := struct {
Hostname string
}{
Hostname: hostname,
}
mock.lockColorLabels.Lock()
mock.calls.ColorLabels = append(mock.calls.ColorLabels, callInfo)
mock.lockColorLabels.Unlock()
return mock.ColorLabelsFunc(hostname)
}
// ColorLabelsCalls gets all the calls that were made to ColorLabels.
// Check the length with:
//
// len(mockedConfig.ColorLabelsCalls())
func (mock *ConfigMock) ColorLabelsCalls() []struct {
Hostname string
} {
var calls []struct {
Hostname string
}
mock.lockColorLabels.RLock()
calls = mock.calls.ColorLabels
mock.lockColorLabels.RUnlock()
return calls
}
// Editor calls EditorFunc.
func (mock *ConfigMock) Editor(hostname string) gh.ConfigEntry {
if mock.EditorFunc == nil {

View file

@ -73,7 +73,7 @@ func NewWithWriter(w io.Writer, isTTY bool, maxWidth int, cs *iostreams.ColorSch
// was not padded. In tests cs.Enabled() is false which allows us to avoid having to fix up
// numerous tests that verify header padding.
var paddingFunc func(int, string) string
if cs.Enabled() {
if cs.Enabled {
paddingFunc = text.PadRight
}

View file

@ -4,6 +4,7 @@ import (
"bytes"
"testing"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/pkg/cmdutil"
@ -91,14 +92,16 @@ func Test_listRun(t *testing.T) {
return cfg
}(),
input: &ListOptions{Hostname: "HOST"},
stdout: `git_protocol=ssh
editor=/usr/bin/vim
prompt=disabled
prefer_editor_prompt=enabled
pager=less
http_unix_socket=
browser=brave
`,
stdout: heredoc.Doc(`
git_protocol=ssh
editor=/usr/bin/vim
prompt=disabled
prefer_editor_prompt=enabled
pager=less
http_unix_socket=
browser=brave
color_labels=disabled
`),
},
}

View file

@ -293,6 +293,17 @@ func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams {
io.SetPager(pager.Value)
}
if ghColorLabels, ghColorLabelsExists := os.LookupEnv("GH_COLOR_LABELS"); ghColorLabelsExists {
switch ghColorLabels {
case "", "0", "false", "no":
io.SetColorLabels(false)
default:
io.SetColorLabels(true)
}
} else if prompt := cfg.ColorLabels(""); prompt.Value == "enabled" {
io.SetColorLabels(true)
}
io.SetAccessibleColorsEnabled(xcolor.IsAccessibleColorsEnabled())
return io

View file

@ -432,6 +432,84 @@ func Test_ioStreams_prompt(t *testing.T) {
}
}
func Test_ioStreams_colorLabels(t *testing.T) {
tests := []struct {
name string
config gh.Config
colorLabelsEnabled bool
env map[string]string
}{
{
name: "default config",
colorLabelsEnabled: false,
},
{
name: "config with colorLabels enabled",
config: enableColorLabelsConfig(),
colorLabelsEnabled: true,
},
{
name: "config with colorLabels disabled",
config: disableColorLabelsConfig(),
colorLabelsEnabled: false,
},
{
name: "colorLabels enabled via `1` in GH_COLOR_LABELS env var",
env: map[string]string{"GH_COLOR_LABELS": "1"},
colorLabelsEnabled: true,
},
{
name: "colorLabels enabled via `true` in GH_COLOR_LABELS env var",
env: map[string]string{"GH_COLOR_LABELS": "true"},
colorLabelsEnabled: true,
},
{
name: "colorLabels enabled via `yes` in GH_COLOR_LABELS env var",
env: map[string]string{"GH_COLOR_LABELS": "yes"},
colorLabelsEnabled: true,
},
{
name: "colorLabels disable via empty string in GH_COLOR_LABELS env var",
env: map[string]string{"GH_COLOR_LABELS": ""},
colorLabelsEnabled: false,
},
{
name: "colorLabels disabled via `0` in GH_COLOR_LABELS env var",
env: map[string]string{"GH_COLOR_LABELS": "0"},
colorLabelsEnabled: false,
},
{
name: "colorLabels disabled via `false` in GH_COLOR_LABELS env var",
env: map[string]string{"GH_COLOR_LABELS": "false"},
colorLabelsEnabled: false,
},
{
name: "colorLabels disabled via `no` in GH_COLOR_LABELS env var",
env: map[string]string{"GH_COLOR_LABELS": "no"},
colorLabelsEnabled: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.env != nil {
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.colorLabelsEnabled, io.ColorLabels())
})
}
}
func TestSSOURL(t *testing.T) {
tests := []struct {
name string
@ -537,3 +615,11 @@ func pagerConfig() gh.Config {
func disablePromptConfig() gh.Config {
return config.NewFromString("prompt: disabled")
}
func disableColorLabelsConfig() gh.Config {
return config.NewFromString("color_labels: disabled")
}
func enableColorLabelsConfig() gh.Config {
return config.NewFromString("color_labels: enabled")
}

View file

@ -138,7 +138,7 @@ func createRun(opts *CreateOptions) error {
processMessage = fmt.Sprintf("Creating gist %s", gistName)
}
}
fmt.Fprintf(errOut, "%s %s\n", cs.Gray("-"), processMessage)
fmt.Fprintf(errOut, "%s %s\n", cs.Muted("-"), processMessage)
httpClient, err := opts.HttpClient()
if err != nil {

View file

@ -654,50 +654,57 @@ func Test_highlightMatch(t *testing.T) {
tests := []struct {
name string
input string
color bool
cs *iostreams.ColorScheme
want string
}{
{
name: "single match",
input: "Octo",
cs: &iostreams.ColorScheme{},
want: "Octo",
},
{
name: "single match (color)",
input: "Octo",
color: true,
want: "\x1b[0;30;43mOcto\x1b[0m",
cs: &iostreams.ColorScheme{
Enabled: true,
},
want: "\x1b[0;30;43mOcto\x1b[0m",
},
{
name: "single match with extra",
input: "Hello, Octocat!",
cs: &iostreams.ColorScheme{},
want: "Hello, Octocat!",
},
{
name: "single match with extra (color)",
input: "Hello, Octocat!",
color: true,
want: "\x1b[0;34mHello, \x1b[0m\x1b[0;30;43mOcto\x1b[0m\x1b[0;34mcat!\x1b[0m",
cs: &iostreams.ColorScheme{
Enabled: true,
},
want: "\x1b[0;34mHello, \x1b[0m\x1b[0;30;43mOcto\x1b[0m\x1b[0;34mcat!\x1b[0m",
},
{
name: "multiple matches",
input: "Octocat/octo",
cs: &iostreams.ColorScheme{},
want: "Octocat/octo",
},
{
name: "multiple matches (color)",
input: "Octocat/octo",
color: true,
want: "\x1b[0;30;43mOcto\x1b[0m\x1b[0;34mcat/\x1b[0m\x1b[0;30;43mocto\x1b[0m",
cs: &iostreams.ColorScheme{
Enabled: true,
},
want: "\x1b[0;30;43mOcto\x1b[0m\x1b[0;34mcat/\x1b[0m\x1b[0;30;43mocto\x1b[0m",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cs := iostreams.NewColorScheme(tt.color, false, false, false, iostreams.NoTheme)
matched := false
got, err := highlightMatch(tt.input, regex, &matched, cs.Blue, cs.Highlight)
got, err := highlightMatch(tt.input, regex, &matched, tt.cs.Blue, tt.cs.Highlight)
assert.NoError(t, err)
assert.True(t, matched)
assert.Equal(t, tt.want, got)

View file

@ -230,7 +230,7 @@ func PromptGists(prompter prompter.Prompter, client *http.Client, host string, c
for i, gist := range gists {
gistTime := text.FuzzyAgo(time.Now(), gist.UpdatedAt)
// TODO: support dynamic maxWidth
opts[i] = fmt.Sprintf("%s %s %s", cs.Bold(gist.Filename()), gist.TruncDescription(), cs.Gray(gistTime))
opts[i] = fmt.Sprintf("%s %s %s", cs.Bold(gist.Filename()), gist.TruncDescription(), cs.Muted(gistTime))
}
result, err := prompter.Select("Select a gist", "", opts)

View file

@ -140,7 +140,7 @@ func viewRun(opts *ViewOptions) error {
if len(gist.Files) == 1 || opts.Filename != "" {
return fmt.Errorf("error: file is binary")
}
_, err = fmt.Fprintln(opts.IO.Out, cs.Gray("(skipping rendering binary content)"))
_, err = fmt.Fprintln(opts.IO.Out, cs.Muted("(skipping rendering binary content)"))
return nil
}
@ -197,7 +197,7 @@ func viewRun(opts *ViewOptions) error {
for i, fn := range filenames {
if showFilenames {
fmt.Fprintf(opts.IO.Out, "%s\n\n", cs.Gray(fn))
fmt.Fprintf(opts.IO.Out, "%s\n\n", cs.Muted(fn))
}
if err := render(gist.Files[fn]); err != nil {
return err

View file

@ -38,13 +38,13 @@ func PrintIssues(io *iostreams.IOStreams, now time.Time, prefix string, totalCou
}
table.AddField(text.RemoveExcessiveWhitespace(issue.Title))
table.AddField(issueLabelList(&issue, cs, isTTY))
table.AddTimeField(now, issue.UpdatedAt, cs.Gray)
table.AddTimeField(now, issue.UpdatedAt, cs.Muted)
table.EndRow()
}
_ = table.Render()
remaining := totalCount - len(issues)
if remaining > 0 {
fmt.Fprintf(io.Out, cs.Gray("%sAnd %d more\n"), prefix, remaining)
fmt.Fprintf(io.Out, cs.Muted("%sAnd %d more\n"), prefix, remaining)
}
}
@ -56,7 +56,7 @@ func issueLabelList(issue *api.Issue, cs *iostreams.ColorScheme, colorize bool)
labelNames := make([]string, 0, len(issue.Labels.Nodes))
for _, label := range issue.Labels.Nodes {
if colorize {
labelNames = append(labelNames, cs.HexToRGB(label.Color, label.Name))
labelNames = append(labelNames, cs.Label(label.Color, label.Name))
} else {
labelNames = append(labelNames, label.Name)
}

View file

@ -228,7 +228,7 @@ func printHumanIssuePreview(opts *ViewOptions, baseRepo ghrepo.Interface, issue
var md string
var err error
if issue.Body == "" {
md = fmt.Sprintf("\n %s\n\n", cs.Gray("No description provided"))
md = fmt.Sprintf("\n %s\n\n", cs.Muted("No description provided"))
} else {
md, err = markdown.Render(issue.Body,
markdown.WithTheme(opts.IO.TerminalTheme()),
@ -250,7 +250,7 @@ func printHumanIssuePreview(opts *ViewOptions, baseRepo ghrepo.Interface, issue
}
// Footer
fmt.Fprintf(out, cs.Gray("View this issue on GitHub: %s\n"), issue.URL)
fmt.Fprintf(out, cs.Muted("View this issue on GitHub: %s\n"), issue.URL)
return nil
}
@ -317,7 +317,7 @@ func issueLabelList(issue *api.Issue, cs *iostreams.ColorScheme) string {
if cs == nil {
labelNames[i] = label.Name
} else {
labelNames[i] = cs.HexToRGB(label.Color, label.Name)
labelNames[i] = cs.Label(label.Color, label.Name)
}
}

View file

@ -137,7 +137,12 @@ func printLabels(io *iostreams.IOStreams, labels []label) error {
table := tableprinter.New(io, tableprinter.WithHeader("NAME", "DESCRIPTION", "COLOR"))
for _, label := range labels {
table.AddField(label.Name, tableprinter.WithColor(cs.ColorFromRGB(label.Color)))
// Colorize the label using tableprinter's WithColor function for it to handle non-TTY situations
labelColor := tableprinter.WithColor(func(s string) string {
return cs.Label(label.Color, s)
})
table.AddField(label.Name, labelColor)
table.AddField(label.Description)
table.AddField("#" + label.Color)

View file

@ -30,7 +30,7 @@ func addRow(tp *tableprinter.TablePrinter, io *iostreams.IOStreams, o check) {
markColor = cs.Yellow
case "skipping", "cancel":
mark = "-"
markColor = cs.Gray
markColor = cs.Muted
}
if io.IsStdoutTTY() {

View file

@ -879,37 +879,37 @@ func renderPullRequestPlain(w io.Writer, params map[string]interface{}, state *s
}
func renderPullRequestTTY(io *iostreams.IOStreams, params map[string]interface{}, state *shared.IssueMetadataState) error {
iofmt := io.ColorScheme()
cs := io.ColorScheme()
out := io.Out
fmt.Fprint(out, "Would have created a Pull Request with:\n")
fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Title"), params["title"].(string))
fmt.Fprintf(out, "%s: %t\n", iofmt.Bold("Draft"), params["draft"])
fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Base"), params["baseRefName"])
fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Head"), params["headRefName"])
fmt.Fprintf(out, "%s: %s\n", cs.Bold("Title"), params["title"].(string))
fmt.Fprintf(out, "%s: %t\n", cs.Bold("Draft"), params["draft"])
fmt.Fprintf(out, "%s: %s\n", cs.Bold("Base"), params["baseRefName"])
fmt.Fprintf(out, "%s: %s\n", cs.Bold("Head"), params["headRefName"])
if len(state.Labels) != 0 {
fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Labels"), strings.Join(state.Labels, ", "))
fmt.Fprintf(out, "%s: %s\n", cs.Bold("Labels"), strings.Join(state.Labels, ", "))
}
if len(state.Reviewers) != 0 {
fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Reviewers"), strings.Join(state.Reviewers, ", "))
fmt.Fprintf(out, "%s: %s\n", cs.Bold("Reviewers"), strings.Join(state.Reviewers, ", "))
}
if len(state.Assignees) != 0 {
fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Assignees"), strings.Join(state.Assignees, ", "))
fmt.Fprintf(out, "%s: %s\n", cs.Bold("Assignees"), strings.Join(state.Assignees, ", "))
}
if len(state.Milestones) != 0 {
fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Milestones"), strings.Join(state.Milestones, ", "))
fmt.Fprintf(out, "%s: %s\n", cs.Bold("Milestones"), strings.Join(state.Milestones, ", "))
}
if len(state.Projects) != 0 {
fmt.Fprintf(out, "%s: %s\n", iofmt.Bold("Projects"), strings.Join(state.Projects, ", "))
fmt.Fprintf(out, "%s: %s\n", cs.Bold("Projects"), strings.Join(state.Projects, ", "))
}
fmt.Fprintf(out, "%s: %t\n", iofmt.Bold("MaintainerCanModify"), params["maintainerCanModify"])
fmt.Fprintf(out, "%s: %t\n", cs.Bold("MaintainerCanModify"), params["maintainerCanModify"])
fmt.Fprintf(out, "%s\n", iofmt.Bold("Body:"))
fmt.Fprintf(out, "%s\n", cs.Bold("Body:"))
// Body
var md string
var err error
if len(params["body"].(string)) == 0 {
md = fmt.Sprintf("%s\n", iofmt.Gray("No description provided"))
md = fmt.Sprintf("%s\n", cs.Muted("No description provided"))
} else {
md, err = markdown.Render(params["body"].(string),
markdown.WithTheme(io.TerminalTheme()),

View file

@ -191,7 +191,7 @@ func reviewRun(opts *ReviewOptions) error {
switch reviewData.State {
case api.ReviewComment:
fmt.Fprintf(opts.IO.ErrOut, "%s Reviewed pull request %s#%d\n", cs.Gray("-"), ghrepo.FullName(baseRepo), pr.Number)
fmt.Fprintf(opts.IO.ErrOut, "%s Reviewed pull request %s#%d\n", cs.Muted("-"), ghrepo.FullName(baseRepo), pr.Number)
case api.ReviewApprove:
fmt.Fprintf(opts.IO.ErrOut, "%s Approved pull request %s#%d\n", cs.SuccessIcon(), ghrepo.FullName(baseRepo), pr.Number)
case api.ReviewRequestChanges:

View file

@ -62,7 +62,7 @@ func CommentList(io *iostreams.IOStreams, comments api.Comments, reviews api.Pul
hiddenCount := totalCount - retrievedCount
if preview && hiddenCount > 0 {
fmt.Fprint(&b, cs.Gray(fmt.Sprintf("———————— Not showing %s ————————", text.Pluralize(hiddenCount, "comment"))))
fmt.Fprint(&b, cs.Muted(fmt.Sprintf("———————— Not showing %s ————————", text.Pluralize(hiddenCount, "comment"))))
fmt.Fprintf(&b, "\n\n\n")
}
@ -79,7 +79,7 @@ func CommentList(io *iostreams.IOStreams, comments api.Comments, reviews api.Pul
}
if preview && hiddenCount > 0 {
fmt.Fprint(&b, cs.Gray("Use --comments to view the full conversation"))
fmt.Fprint(&b, cs.Muted("Use --comments to view the full conversation"))
fmt.Fprintln(&b)
}
@ -122,7 +122,7 @@ func formatComment(io *iostreams.IOStreams, comment Comment, newest bool) (strin
var md string
var err error
if comment.Content() == "" {
md = fmt.Sprintf("\n %s\n\n", cs.Gray("No body provided"))
md = fmt.Sprintf("\n %s\n\n", cs.Muted("No body provided"))
} else {
md, err = markdown.Render(comment.Content(),
markdown.WithTheme(io.TerminalTheme()),
@ -135,7 +135,7 @@ func formatComment(io *iostreams.IOStreams, comment Comment, newest bool) (strin
// Footer
if comment.Link() != "" {
fmt.Fprintf(&b, cs.Gray("View the full review: %s\n\n"), comment.Link())
fmt.Fprintf(&b, cs.Muted("View the full review: %s\n\n"), comment.Link())
}
return b.String(), nil

View file

@ -60,7 +60,7 @@ func PrintHeader(io *iostreams.IOStreams, s string) {
}
func PrintMessage(io *iostreams.IOStreams, s string) {
fmt.Fprintln(io.Out, io.ColorScheme().Gray(s))
fmt.Fprintln(io.Out, io.ColorScheme().Muted(s))
}
func ListNoResults(repoName string, itemName string, hasFilters bool) error {
@ -83,7 +83,7 @@ func ListHeader(repoName string, itemName string, matchCount int, totalMatchCoun
}
func PrCheckStatusSummaryWithColor(cs *iostreams.ColorScheme, checks api.PullRequestChecksStatus) string {
var summary = cs.Gray("No checks")
var summary = cs.Muted("No checks")
if checks.Total > 0 {
if checks.Failing > 0 {
if checks.Failing == checks.Total {

View file

@ -316,6 +316,6 @@ func printPrs(io *iostreams.IOStreams, totalCount int, prs ...api.PullRequest) {
}
remaining := totalCount - len(prs)
if remaining > 0 {
fmt.Fprintf(w, cs.Gray(" And %d more\n"), remaining)
fmt.Fprintln(w, cs.Mutedf(" And %d more", remaining))
}
}

View file

@ -260,7 +260,7 @@ func printHumanPrPreview(opts *ViewOptions, baseRepo ghrepo.Interface, pr *api.P
var md string
var err error
if pr.Body == "" {
md = fmt.Sprintf("\n %s\n\n", cs.Gray("No description provided"))
md = fmt.Sprintf("\n %s\n\n", cs.Muted("No description provided"))
} else {
md, err = markdown.Render(pr.Body,
markdown.WithTheme(opts.IO.TerminalTheme()),
@ -282,7 +282,7 @@ func printHumanPrPreview(opts *ViewOptions, baseRepo ghrepo.Interface, pr *api.P
}
// Footer
fmt.Fprintf(out, cs.Gray("View this pull request on GitHub: %s\n"), pr.URL)
fmt.Fprintf(out, cs.Muted("View this pull request on GitHub: %s\n"), pr.URL)
return nil
}
@ -423,7 +423,7 @@ func prLabelList(pr api.PullRequest, cs *iostreams.ColorScheme) string {
labelNames := make([]string, 0, len(pr.Labels.Nodes))
for _, label := range pr.Labels.Nodes {
labelNames = append(labelNames, cs.HexToRGB(label.Color, label.Name))
labelNames = append(labelNames, cs.Label(label.Color, label.Name))
}
list := strings.Join(labelNames, ", ")

View file

@ -129,19 +129,19 @@ func viewRun(opts *ViewOptions) error {
}
func renderReleaseTTY(io *iostreams.IOStreams, release *shared.Release) error {
iofmt := io.ColorScheme()
cs := io.ColorScheme()
w := io.Out
fmt.Fprintf(w, "%s\n", iofmt.Bold(release.TagName))
fmt.Fprintf(w, "%s\n", cs.Bold(release.TagName))
if release.IsDraft {
fmt.Fprintf(w, "%s • ", iofmt.Red("Draft"))
fmt.Fprintf(w, "%s • ", cs.Red("Draft"))
} else if release.IsPrerelease {
fmt.Fprintf(w, "%s • ", iofmt.Yellow("Pre-release"))
fmt.Fprintf(w, "%s • ", cs.Yellow("Pre-release"))
}
if release.IsDraft {
fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("%s created this %s", release.Author.Login, text.FuzzyAgo(time.Now(), release.CreatedAt))))
fmt.Fprintln(w, cs.Mutedf("%s created this %s", release.Author.Login, text.FuzzyAgo(time.Now(), release.CreatedAt)))
} else {
fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("%s released this %s", release.Author.Login, text.FuzzyAgo(time.Now(), *release.PublishedAt))))
fmt.Fprintln(w, cs.Mutedf("%s released this %s", release.Author.Login, text.FuzzyAgo(time.Now(), *release.PublishedAt)))
}
renderedDescription, err := markdown.Render(release.Body,
@ -153,7 +153,7 @@ func renderReleaseTTY(io *iostreams.IOStreams, release *shared.Release) error {
fmt.Fprintln(w, renderedDescription)
if len(release.Assets) > 0 {
fmt.Fprintf(w, "%s\n", iofmt.Bold("Assets"))
fmt.Fprintln(w, cs.Bold("Assets"))
//nolint:staticcheck // SA1019: Showing NAME|SIZE headers adds nothing to table.
table := tableprinter.New(io, tableprinter.NoHeader)
for _, a := range release.Assets {
@ -168,7 +168,7 @@ func renderReleaseTTY(io *iostreams.IOStreams, release *shared.Release) error {
fmt.Fprint(w, "\n")
}
fmt.Fprintf(w, "%s\n", iofmt.Gray(fmt.Sprintf("View on GitHub: %s", release.URL)))
fmt.Fprintln(w, cs.Mutedf("View on GitHub: %s", release.URL))
return nil
}

View file

@ -119,9 +119,9 @@ func renderLicense(license *api.License, opts *ViewOptions) error {
cs := opts.IO.ColorScheme()
var out strings.Builder
if opts.IO.IsStdoutTTY() {
out.WriteString(fmt.Sprintf("\n%s\n", cs.Gray(license.Description)))
out.WriteString(fmt.Sprintf("\n%s\n", cs.Grayf("To implement: %s", license.Implementation)))
out.WriteString(fmt.Sprintf("\n%s\n\n", cs.Grayf("For more information, see: %s", license.HTMLURL)))
out.WriteString(fmt.Sprintf("\n%s\n", cs.Muted(license.Description)))
out.WriteString(fmt.Sprintf("\n%s\n", cs.Mutedf("To implement: %s", license.Implementation)))
out.WriteString(fmt.Sprintf("\n%s\n\n", cs.Mutedf("For more information, see: %s", license.HTMLURL)))
}
out.WriteString(license.Body)
_, err := opts.IO.Out.Write([]byte(out.String()))

View file

@ -181,7 +181,7 @@ func viewRun(opts *ViewOptions) error {
var readmeContent string
if readme == nil {
readmeContent = cs.Gray("This repository does not have a README")
readmeContent = cs.Muted("This repository does not have a README")
} else if isMarkdownFile(readme.Filename) {
var err error
readmeContent, err = markdown.Render(readme.Content,
@ -197,7 +197,7 @@ func viewRun(opts *ViewOptions) error {
description := repo.Description
if description == "" {
description = cs.Gray("No description provided")
description = cs.Muted("No description provided")
}
repoData := struct {
@ -209,7 +209,7 @@ func viewRun(opts *ViewOptions) error {
FullName: cs.Bold(fullName),
Description: description,
Readme: readmeContent,
View: cs.Gray(fmt.Sprintf("View this repository on GitHub: %s", openURL)),
View: cs.Mutedf("View this repository on GitHub: %s", openURL),
}
return tmpl.Execute(stdout, repoData)

View file

@ -87,7 +87,7 @@ func isRootCmd(command *cobra.Command) bool {
return command != nil && !command.HasParent()
}
func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, args []string) {
func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, _ []string) {
flags := command.Flags()
if isRootCmd(command) {

View file

@ -81,6 +81,9 @@ var HelpTopics = []helpTopic{
%[1]sCLICOLOR_FORCE%[1]s: set to a value other than %[1]s0%[1]s to keep ANSI colors in output
even when the output is piped.
%[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_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

View file

@ -52,7 +52,8 @@ func RenderAnnotations(cs *iostreams.ColorScheme, annotations []Annotation) stri
for _, a := range annotations {
lines = append(lines, fmt.Sprintf("%s %s", AnnotationSymbol(cs, a), a.Message))
lines = append(lines, cs.Grayf("%s: %s#%d\n", a.JobName, a.Path, a.StartLine))
// Following newline is essential for spacing between annotations
lines = append(lines, cs.Mutedf("%s: %s#%d\n", a.JobName, a.Path, a.StartLine))
}
return strings.Join(lines, "\n")

View file

@ -575,7 +575,7 @@ func Symbol(cs *iostreams.ColorScheme, status Status, conclusion Conclusion) (st
case Success:
return cs.SuccessIconWithColor(noColor), cs.Green
case Skipped, Neutral:
return "-", cs.Gray
return "-", cs.Muted
default:
return cs.FailureIconWithColor(noColor), cs.Red
}

View file

@ -397,7 +397,7 @@ func runView(opts *ViewOptions) error {
for _, a := range artifacts {
expiredBadge := ""
if a.Expired {
expiredBadge = cs.Gray(" (expired)")
expiredBadge = cs.Muted(" (expired)")
}
fmt.Fprintf(out, "%s%s\n", a.Name, expiredBadge)
}
@ -411,7 +411,7 @@ func runView(opts *ViewOptions) error {
} else {
fmt.Fprintf(out, "For more information about a job, try: gh run view --job=<job-id>\n")
}
fmt.Fprintf(out, cs.Gray("View this run on GitHub: %s\n"), run.URL)
fmt.Fprintln(out, cs.Mutedf("View this run on GitHub: %s", run.URL))
if opts.ExitStatus && shared.IsFailureState(run.Conclusion) {
return cmdutil.SilentError
@ -423,7 +423,7 @@ func runView(opts *ViewOptions) error {
} else {
fmt.Fprintf(out, "To see the full job log, try: gh run view --log --job=%d\n", selectedJob.ID)
}
fmt.Fprintf(out, cs.Gray("View this run on GitHub: %s\n"), run.URL)
fmt.Fprintln(out, cs.Mutedf("View this run on GitHub: %s", run.URL))
if opts.ExitStatus && shared.IsFailureState(selectedJob.Conclusion) {
return cmdutil.SilentError

View file

@ -161,7 +161,7 @@ func displayResults(io *iostreams.IOStreams, now time.Time, results search.Commi
tp.AddField(commit.Sha)
tp.AddField(text.RemoveExcessiveWhitespace(commit.Info.Message))
tp.AddField(commit.Author.Login)
tp.AddTimeField(now, commit.Info.Author.Date, cs.Gray)
tp.AddTimeField(now, commit.Info.Author.Date, cs.Muted)
tp.EndRow()
}
if io.IsStdoutTTY() {

View file

@ -171,14 +171,14 @@ func displayResults(io *iostreams.IOStreams, now time.Time, results search.Repos
tags = append(tags, "archived")
}
info := strings.Join(tags, ", ")
infoColor := cs.Gray
infoColor := cs.Muted
if repo.IsPrivate {
infoColor = cs.Yellow
}
tp.AddField(repo.FullName, tableprinter.WithColor(cs.Bold))
tp.AddField(text.RemoveExcessiveWhitespace(repo.Description))
tp.AddField(info, tableprinter.WithColor(infoColor))
tp.AddTimeField(now, repo.UpdatedAt, cs.Gray)
tp.AddTimeField(now, repo.UpdatedAt, cs.Muted)
tp.EndRow()
}
if io.IsStdoutTTY() {

View file

@ -132,7 +132,7 @@ func displayIssueResults(io *iostreams.IOStreams, now time.Time, et EntityType,
}
tp.AddField(text.RemoveExcessiveWhitespace(issue.Title))
tp.AddField(listIssueLabels(&issue, cs, tp.IsTTY()))
tp.AddTimeField(now, issue.UpdatedAt, cs.Gray)
tp.AddTimeField(now, issue.UpdatedAt, cs.Muted)
tp.EndRow()
}
@ -158,7 +158,7 @@ func listIssueLabels(issue *search.Issue, cs *iostreams.ColorScheme, colorize bo
labelNames := make([]string, 0, len(issue.Labels))
for _, label := range issue.Labels {
if colorize {
labelNames = append(labelNames, cs.HexToRGB(label.Color, label.Name))
labelNames = append(labelNames, cs.Label(label.Color, label.Name))
} else {
labelNames = append(labelNames, label.Name)
}

View file

@ -740,7 +740,7 @@ func statusRun(opts *StatusOptions) error {
errs := sg.authErrors.ToSlice()
sort.Strings(errs)
for _, msg := range errs {
fmt.Fprintln(out, cs.Gray(fmt.Sprintf("warning: %s", msg)))
fmt.Fprintln(out, cs.Mutedf("warning: %s", msg))
}
}

View file

@ -175,7 +175,7 @@ func viewWorkflowContent(opts *ViewOptions, client *api.Client, repo ghrepo.Inte
out := opts.IO.Out
fileName := workflow.Base()
fmt.Fprintf(out, "%s - %s\n", cs.Bold(workflow.Name), cs.Gray(fileName))
fmt.Fprintf(out, "%s - %s\n", cs.Bold(workflow.Name), cs.Muted(fileName))
fmt.Fprintf(out, "ID: %s", cs.Cyanf("%d", workflow.ID))
codeBlock := fmt.Sprintf("```yaml\n%s\n```", yaml)

View file

@ -41,33 +41,24 @@ var (
}
)
// NewColorScheme initializes color logic based on provided terminal capabilities.
// Logic dealing with terminal theme detected, such as whether color is enabled, 8-bit color supported, true color supported,
// and terminal theme detected.
func NewColorScheme(enabled, is256enabled, trueColor, accessibleColors bool, theme string) *ColorScheme {
return &ColorScheme{
enabled: enabled,
is256enabled: is256enabled,
hasTrueColor: trueColor,
accessibleColors: accessibleColors,
theme: theme,
}
}
// ColorScheme controls how text is colored based upon terminal capabilities and user preferences.
type ColorScheme struct {
enabled bool
is256enabled bool
hasTrueColor bool
accessibleColors bool
theme string
}
func (c *ColorScheme) Enabled() bool {
return c.enabled
// Enabled is whether color is used at all.
Enabled bool
// EightBitColor is whether the terminal supports 8-bit, 256 colors.
EightBitColor bool
// TrueColor is whether the terminal supports 24-bit, 16 million colors.
TrueColor bool
// Accessible is whether colors must be base 16 colors that users can customize in terminal preferences.
Accessible bool
// ColorLabels is whether labels are colored based on their truecolor RGB hex color.
ColorLabels bool
// Theme is the terminal background color theme used to contextually color text for light, dark, or none at all.
Theme string
}
func (c *ColorScheme) Bold(t string) string {
if !c.enabled {
if !c.Enabled {
return t
}
return bold(t)
@ -79,16 +70,16 @@ func (c *ColorScheme) Boldf(t string, args ...interface{}) string {
func (c *ColorScheme) Muted(t string) string {
// Fallback to previous logic if accessible colors preview is disabled.
if !c.accessibleColors {
if !c.Accessible {
return c.Gray(t)
}
// Muted text is only stylized if color is enabled.
if !c.enabled {
if !c.Enabled {
return t
}
switch c.theme {
switch c.Theme {
case LightTheme:
return lightThemeMuted(t)
case DarkTheme:
@ -103,7 +94,7 @@ func (c *ColorScheme) Mutedf(t string, args ...interface{}) string {
}
func (c *ColorScheme) Red(t string) string {
if !c.enabled {
if !c.Enabled {
return t
}
return red(t)
@ -114,7 +105,7 @@ func (c *ColorScheme) Redf(t string, args ...interface{}) string {
}
func (c *ColorScheme) Yellow(t string) string {
if !c.enabled {
if !c.Enabled {
return t
}
return yellow(t)
@ -125,7 +116,7 @@ func (c *ColorScheme) Yellowf(t string, args ...interface{}) string {
}
func (c *ColorScheme) Green(t string) string {
if !c.enabled {
if !c.Enabled {
return t
}
return green(t)
@ -136,30 +127,30 @@ func (c *ColorScheme) Greenf(t string, args ...interface{}) string {
}
func (c *ColorScheme) GreenBold(t string) string {
if !c.enabled {
if !c.Enabled {
return t
}
return greenBold(t)
}
// Use Muted instead for thematically contrasting color.
// Deprecated: Use Muted instead for thematically contrasting color.
func (c *ColorScheme) Gray(t string) string {
if !c.enabled {
if !c.Enabled {
return t
}
if c.is256enabled {
if c.EightBitColor {
return gray256(t)
}
return gray(t)
}
// Use Mutedf instead for thematically contrasting color.
// Deprecated: Use Mutedf instead for thematically contrasting color.
func (c *ColorScheme) Grayf(t string, args ...interface{}) string {
return c.Gray(fmt.Sprintf(t, args...))
}
func (c *ColorScheme) Magenta(t string) string {
if !c.enabled {
if !c.Enabled {
return t
}
return magenta(t)
@ -170,7 +161,7 @@ func (c *ColorScheme) Magentaf(t string, args ...interface{}) string {
}
func (c *ColorScheme) Cyan(t string) string {
if !c.enabled {
if !c.Enabled {
return t
}
return cyan(t)
@ -181,14 +172,14 @@ func (c *ColorScheme) Cyanf(t string, args ...interface{}) string {
}
func (c *ColorScheme) CyanBold(t string) string {
if !c.enabled {
if !c.Enabled {
return t
}
return cyanBold(t)
}
func (c *ColorScheme) Blue(t string) string {
if !c.enabled {
if !c.Enabled {
return t
}
return blue(t)
@ -219,7 +210,7 @@ func (c *ColorScheme) FailureIconWithColor(colo func(string) string) string {
}
func (c *ColorScheme) HighlightStart() string {
if !c.enabled {
if !c.Enabled {
return ""
}
@ -227,7 +218,7 @@ func (c *ColorScheme) HighlightStart() string {
}
func (c *ColorScheme) Highlight(t string) string {
if !c.enabled {
if !c.Enabled {
return t
}
@ -235,7 +226,7 @@ func (c *ColorScheme) Highlight(t string) string {
}
func (c *ColorScheme) Reset() string {
if !c.enabled {
if !c.Enabled {
return ""
}
@ -255,7 +246,7 @@ func (c *ColorScheme) ColorFromString(s string) func(string) string {
case "green":
fn = c.Green
case "gray":
fn = c.Gray
fn = c.Muted
case "magenta":
fn = c.Magenta
case "cyan":
@ -271,17 +262,9 @@ func (c *ColorScheme) ColorFromString(s string) func(string) string {
return fn
}
// ColorFromRGB returns a function suitable for TablePrinter.AddField
// that calls HexToRGB, coloring text if supported by the terminal.
func (c *ColorScheme) ColorFromRGB(hex string) func(string) string {
return func(s string) string {
return c.HexToRGB(hex, s)
}
}
// HexToRGB uses the given hex to color x if supported by the terminal.
func (c *ColorScheme) HexToRGB(hex string, x string) string {
if !c.enabled || !c.hasTrueColor || len(hex) != 6 {
// Label stylizes text based on label's RGB hex color.
func (c *ColorScheme) Label(hex string, x string) string {
if !c.Enabled || !c.TrueColor || !c.ColorLabels || len(hex) != 6 {
return x
}
@ -293,11 +276,11 @@ func (c *ColorScheme) HexToRGB(hex string, x string) string {
func (c *ColorScheme) TableHeader(t string) string {
// Table headers are only stylized if color is enabled including underline modifier.
if !c.enabled {
if !c.Enabled {
return t
}
switch c.theme {
switch c.Theme {
case DarkTheme:
return darkThemeTableHeader(t)
case LightTheme:

View file

@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestColorFromRGB(t *testing.T) {
func TestLabel(t *testing.T) {
tests := []struct {
name string
hex string
@ -20,77 +20,57 @@ func TestColorFromRGB(t *testing.T) {
hex: "fc0303",
text: "red",
wants: "\033[38;2;252;3;3mred\033[0m",
cs: NewColorScheme(true, true, true, false, NoTheme),
cs: &ColorScheme{
Enabled: true,
EightBitColor: true,
TrueColor: true,
ColorLabels: true,
},
},
{
name: "no truecolor",
hex: "fc0303",
text: "red",
wants: "red",
cs: NewColorScheme(true, true, false, false, NoTheme),
cs: &ColorScheme{
Enabled: true,
EightBitColor: true,
ColorLabels: true,
},
},
{
name: "no color",
hex: "fc0303",
text: "red",
wants: "red",
cs: NewColorScheme(false, false, false, false, NoTheme),
cs: &ColorScheme{
ColorLabels: true,
},
},
{
name: "invalid hex",
hex: "fc0",
text: "red",
wants: "red",
cs: NewColorScheme(false, false, false, false, NoTheme),
cs: &ColorScheme{
ColorLabels: true,
},
},
{
name: "no color labels",
hex: "fc0303",
text: "red",
wants: "red",
cs: &ColorScheme{
Enabled: true,
EightBitColor: true,
ColorLabels: true,
},
},
}
for _, tt := range tests {
fn := tt.cs.ColorFromRGB(tt.hex)
assert.Equal(t, tt.wants, fn(tt.text))
}
}
func TestHexToRGB(t *testing.T) {
tests := []struct {
name string
hex string
text string
wants string
cs *ColorScheme
}{
{
name: "truecolor",
hex: "fc0303",
text: "red",
wants: "\033[38;2;252;3;3mred\033[0m",
cs: NewColorScheme(true, true, true, false, NoTheme),
},
{
name: "no truecolor",
hex: "fc0303",
text: "red",
wants: "red",
cs: NewColorScheme(true, true, false, false, NoTheme),
},
{
name: "no color",
hex: "fc0303",
text: "red",
wants: "red",
cs: NewColorScheme(false, false, false, false, NoTheme),
},
{
name: "invalid hex",
hex: "fc0",
text: "red",
wants: "red",
cs: NewColorScheme(false, false, false, false, NoTheme),
},
}
for _, tt := range tests {
output := tt.cs.HexToRGB(tt.hex, tt.text)
output := tt.cs.Label(tt.hex, tt.text)
assert.Equal(t, tt.wants, output)
}
}
@ -108,62 +88,110 @@ func TestTableHeader(t *testing.T) {
expected string
}{
{
name: "when color is disabled, text is not stylized",
cs: NewColorScheme(false, false, false, true, NoTheme),
name: "when color is disabled, text is not stylized",
cs: &ColorScheme{
Accessible: true,
Theme: NoTheme,
},
input: "this should not be stylized",
expected: "this should not be stylized",
},
{
name: "when 4-bit color is enabled but no theme, 4-bit default color and underline are used",
cs: NewColorScheme(true, false, false, true, NoTheme),
name: "when 4-bit color is enabled but no theme, 4-bit default color and underline are used",
cs: &ColorScheme{
Enabled: true,
Accessible: true,
Theme: NoTheme,
},
input: "this should have no explicit color but underlined",
expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset),
},
{
name: "when 4-bit color is enabled and theme is light, 4-bit dark color and underline are used",
cs: NewColorScheme(true, false, false, true, LightTheme),
name: "when 4-bit color is enabled and theme is light, 4-bit dark color and underline are used",
cs: &ColorScheme{
Enabled: true,
Accessible: true,
Theme: LightTheme,
},
input: "this should have dark foreground color and underlined",
expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset),
},
{
name: "when 4-bit color is enabled and theme is dark, 4-bit light color and underline are used",
cs: NewColorScheme(true, false, false, true, DarkTheme),
name: "when 4-bit color is enabled and theme is dark, 4-bit light color and underline are used",
cs: &ColorScheme{
Enabled: true,
Accessible: true,
Theme: DarkTheme,
},
input: "this should have light foreground color and underlined",
expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset),
},
{
name: "when 8-bit color is enabled but no theme, 4-bit default color and underline are used",
cs: NewColorScheme(true, true, false, true, NoTheme),
name: "when 8-bit color is enabled but no theme, 4-bit default color and underline are used",
cs: &ColorScheme{
Enabled: true,
EightBitColor: true,
Accessible: true,
Theme: NoTheme,
},
input: "this should have no explicit color but underlined",
expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset),
},
{
name: "when 8-bit color is enabled and theme is light, 4-bit dark color and underline are used",
cs: NewColorScheme(true, true, false, true, LightTheme),
name: "when 8-bit color is enabled and theme is light, 4-bit dark color and underline are used",
cs: &ColorScheme{
Enabled: true,
EightBitColor: true,
Accessible: true,
Theme: LightTheme,
},
input: "this should have dark foreground color and underlined",
expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset),
},
{
name: "when 8-bit color is true and theme is dark, 4-bit light color and underline are used",
cs: NewColorScheme(true, true, false, true, DarkTheme),
name: "when 8-bit color is true and theme is dark, 4-bit light color and underline are used",
cs: &ColorScheme{
Enabled: true,
EightBitColor: true,
Accessible: true,
Theme: DarkTheme,
},
input: "this should have light foreground color and underlined",
expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset),
},
{
name: "when 24-bit color is enabled but no theme, 4-bit default color and underline are used",
cs: NewColorScheme(true, true, true, true, NoTheme),
name: "when 24-bit color is enabled but no theme, 4-bit default color and underline are used",
cs: &ColorScheme{
Enabled: true,
EightBitColor: true,
TrueColor: true,
Accessible: true,
Theme: NoTheme,
},
input: "this should have no explicit color but underlined",
expected: fmt.Sprintf("%sthis should have no explicit color but underlined%s", defaultUnderline, reset),
},
{
name: "when 24-bit color is enabled and theme is light, 4-bit dark color and underline are used",
cs: NewColorScheme(true, true, true, true, LightTheme),
name: "when 24-bit color is enabled and theme is light, 4-bit dark color and underline are used",
cs: &ColorScheme{
Enabled: true,
EightBitColor: true,
TrueColor: true,
Accessible: true,
Theme: LightTheme,
},
input: "this should have dark foreground color and underlined",
expected: fmt.Sprintf("%sthis should have dark foreground color and underlined%s", brightBlackUnderline, reset),
},
{
name: "when 24-bit color is true and theme is dark, 4-bit light color and underline are used",
cs: NewColorScheme(true, true, true, true, DarkTheme),
name: "when 24-bit color is true and theme is dark, 4-bit light color and underline are used",
cs: &ColorScheme{
Enabled: true,
EightBitColor: true,
TrueColor: true,
Accessible: true,
Theme: DarkTheme,
},
input: "this should have light foreground color and underlined",
expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset),
},
@ -191,43 +219,70 @@ func TestMuted(t *testing.T) {
}{
{
name: "when color is disabled but accessible colors are disabled, text is not stylized",
cs: NewColorScheme(false, false, false, false, NoTheme),
cs: &ColorScheme{},
input: "this should not be stylized",
expected: "this should not be stylized",
},
{
name: "when 4-bit color is enabled but accessible colors are disabled, legacy 4-bit gray color is used",
cs: NewColorScheme(true, false, false, false, NoTheme),
name: "when 4-bit color is enabled but accessible colors are disabled, legacy 4-bit gray color is used",
cs: &ColorScheme{
Enabled: true,
},
input: "this should be 4-bit gray",
expected: fmt.Sprintf("%sthis should be 4-bit gray%s", gray4bit, reset),
},
{
name: "when 8-bit color is enabled but accessible colors are disabled, legacy 8-bit gray color is used",
cs: NewColorScheme(true, true, false, false, NoTheme),
name: "when 8-bit color is enabled but accessible colors are disabled, legacy 8-bit gray color is used",
cs: &ColorScheme{
Enabled: true,
EightBitColor: true,
},
input: "this should be 8-bit gray",
expected: fmt.Sprintf("%sthis should be 8-bit gray%s", gray8bit, reset),
},
{
name: "when 24-bit color is enabled but accessible colors are disabled, legacy 8-bit gray color is used",
cs: NewColorScheme(true, true, true, false, NoTheme),
name: "when 24-bit color is enabled but accessible colors are disabled, legacy 8-bit gray color is used",
cs: &ColorScheme{
Enabled: true,
EightBitColor: true,
TrueColor: true,
},
input: "this should be 8-bit gray",
expected: fmt.Sprintf("%sthis should be 8-bit gray%s", gray8bit, reset),
},
{
name: "when 4-bit color is enabled and theme is dark, 4-bit light color is used",
cs: NewColorScheme(true, true, true, true, DarkTheme),
name: "when 4-bit color is enabled and theme is dark, 4-bit light color is used",
cs: &ColorScheme{
Enabled: true,
EightBitColor: true,
TrueColor: true,
Accessible: true,
Theme: DarkTheme,
},
input: "this should be 4-bit dim black",
expected: fmt.Sprintf("%sthis should be 4-bit dim black%s", dimBlack4bit, reset),
},
{
name: "when 4-bit color is enabled and theme is light, 4-bit dark color is used",
cs: NewColorScheme(true, true, true, true, LightTheme),
name: "when 4-bit color is enabled and theme is light, 4-bit dark color is used",
cs: &ColorScheme{
Enabled: true,
EightBitColor: true,
TrueColor: true,
Accessible: true,
Theme: LightTheme,
},
input: "this should be 4-bit bright black",
expected: fmt.Sprintf("%sthis should be 4-bit bright black%s", brightBlack4bit, reset),
},
{
name: "when 4-bit color is enabled but no theme, 4-bit default color is used",
cs: NewColorScheme(true, true, true, true, NoTheme),
name: "when 4-bit color is enabled but no theme, 4-bit default color is used",
cs: &ColorScheme{
Enabled: true,
EightBitColor: true,
TrueColor: true,
Accessible: true,
Theme: NoTheme,
},
input: "this should have no explicit color",
expected: "this should have no explicit color",
},

View file

@ -5,7 +5,7 @@ package iostreams
import "os"
func hasAlternateScreenBuffer(hasTrueColor bool) bool {
func hasAlternateScreenBuffer(_ bool) bool {
// on non-Windows, we just assume that alternate screen buffer is supported in most cases
return os.Getenv("TERM") != "dumb"
}

View file

@ -72,6 +72,7 @@ type IOStreams struct {
colorOverride bool
colorEnabled bool
colorLabels bool
accessibleColorsEnabled bool
pagerCommand string
@ -103,6 +104,10 @@ func (s *IOStreams) HasTrueColor() bool {
return s.term.IsTrueColorSupported()
}
func (s *IOStreams) ColorLabels() bool {
return s.colorLabels
}
// DetectTerminalTheme is a utility to call before starting the output pager so that the terminal background
// can be reliably detected.
func (s *IOStreams) DetectTerminalTheme() {
@ -135,6 +140,10 @@ func (s *IOStreams) SetColorEnabled(colorEnabled bool) {
s.colorEnabled = colorEnabled
}
func (s *IOStreams) SetColorLabels(colorLabels bool) {
s.colorLabels = colorLabels
}
func (s *IOStreams) SetStdinTTY(isTTY bool) {
s.stdinTTYOverride = true
s.stdinIsTTY = isTTY
@ -367,7 +376,14 @@ func (s *IOStreams) TerminalWidth() int {
}
func (s *IOStreams) ColorScheme() *ColorScheme {
return NewColorScheme(s.ColorEnabled(), s.ColorSupport256(), s.HasTrueColor(), s.AccessibleColorsEnabled(), s.TerminalTheme())
return &ColorScheme{
Enabled: s.ColorEnabled(),
EightBitColor: s.ColorSupport256(),
TrueColor: s.HasTrueColor(),
Accessible: s.AccessibleColorsEnabled(),
ColorLabels: s.ColorLabels(),
Theme: s.TerminalTheme(),
}
}
func (s *IOStreams) ReadUserFile(fn string) ([]byte, error) {