Repository: cli/cli
Pull request #10709 (2025-03-31 19:56:51)

Ensure table datetime columns have thematic, customizable muted text
Fixes github/cli#832

This pull request implements new functions for stylizing text as muted / dim text using customizable, accessible colors.

The new `ColorScheme.Muted` and `ColorScheme.Mutedf` functions should replace existing `ColorScheme.Gray` and `ColorScheme.Grayf` use eventually, but are isolated to the following commands for now:

- `gh cache list`
- `gh codespace list`
- `gh gist list`
- `gh gpg-key list`
- `gh pr list`
- `gh release list`
- `gh repo deploy-key list`
- `gh repo list`
- `gh run list`
- `gh ssh-key list`

## Demo

### Light

In the following screenshots, the customizable, accessible muted text is rendered as orange.

- **Default experience using 8-bit gray**

  <img width="1512" alt="table-datetime-light-default" src="https://github.com/user-attachments/assets/87597236-1394-4697-b466-9307dd0df936" />

- **Accessible experience using environment variable**

  <img width="1512" alt="table-datetime-light-accessible-env" src="https://github.com/user-attachments/assets/38d346b2-1f43-4b73-8950-210d962c3934" />

- **Accessible experience using configuration setting**

  <img width="1512" alt="table-datetime-light-accessible-config" src="https://github.com/user-attachments/assets/86a6d52d-1dd9-4535-a94f-adea3e175a88" />

### Dark

In the following screenshots, the customizable, accessible muted text is rendered as green.

- **Default experience using 8-bit gray**

  <img width="1512" alt="table-datetime-dark-default" src="https://github.com/user-attachments/assets/2d8b0534-b76d-4fcc-8125-d79234b9e972" />

- **Accessible experience using environment variable**

  <img width="1512" alt="table-datetime-dark-accessible-env" src="https://github.com/user-attachments/assets/6e9c00cf-564a-451e-a4df-b8901aabe397" />

- **Accessible experience using configuration setting**

  <img width="1512" alt="table-datetime-dark-accessible-config" src="https://github.com/user-attachments/assets/0d082f3b-f03f-449e-95f4-72436622f0ce" />



Patch:
diff --git a/pkg/cmd/cache/list/list.go b/pkg/cmd/cache/list/list.go
index 9f39876cb..d699e2c3d 100644
--- a/pkg/cmd/cache/list/list.go
+++ b/pkg/cmd/cache/list/list.go
@@ -99,11 +99,12 @@ func listRun(opts *ListOptions) error {
 	}
 	client := api.NewClientFromHTTP(httpClient)
 
+	cs := opts.IO.ColorScheme()
 	opts.IO.StartProgressIndicator()
 	result, err := shared.GetCaches(client, repo, shared.GetCachesOptions{Limit: opts.Limit, Sort: opts.Sort, Order: opts.Order, Key: opts.Key, Ref: opts.Ref})
 	opts.IO.StopProgressIndicator()
 	if err != nil {
-		return fmt.Errorf("%s Failed to get caches: %w", opts.IO.ColorScheme().FailureIcon(), err)
+		return fmt.Errorf("%s Failed to get caches: %w", cs.FailureIcon(), err)
 	}
 
 	if len(result.ActionsCaches) == 0 && opts.Exporter == nil {
@@ -130,11 +131,11 @@ func listRun(opts *ListOptions) error {
 
 	tp := tableprinter.New(opts.IO, tableprinter.WithHeader("ID", "KEY", "SIZE", "CREATED", "ACCESSED"))
 	for _, cache := range result.ActionsCaches {
-		tp.AddField(opts.IO.ColorScheme().Cyan(fmt.Sprintf("%d", cache.Id)))
+		tp.AddField(cs.Cyanf("%d", cache.Id))
 		tp.AddField(cache.Key)
 		tp.AddField(humanFileSize(cache.SizeInBytes))
-		tp.AddTimeField(opts.Now, cache.CreatedAt, opts.IO.ColorScheme().Gray)
-		tp.AddTimeField(opts.Now, cache.LastAccessedAt, opts.IO.ColorScheme().Gray)
+		tp.AddTimeField(opts.Now, cache.CreatedAt, cs.Muted)
+		tp.AddTimeField(opts.Now, cache.LastAccessedAt, cs.Muted)
 		tp.EndRow()
 	}
 
diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go
index c1acccabb..238c4a6a7 100644
--- a/pkg/cmd/codespace/list.go
+++ b/pkg/cmd/codespace/list.go
@@ -152,7 +152,7 @@ func (a *App) List(ctx context.Context, opts *listOptions, exporter cmdutil.Expo
 		case false:
 			nameColor = cs.Yellow
 		case true:
-			nameColor = cs.Gray
+			nameColor = cs.Muted
 		}
 
 		tp.AddField(formattedName, tableprinter.WithColor(nameColor))
@@ -172,7 +172,7 @@ func (a *App) List(ctx context.Context, opts *listOptions, exporter cmdutil.Expo
 		if err != nil {
 			return fmt.Errorf("error parsing date %q: %w", c.CreatedAt, err)
 		}
-		tp.AddTimeField(time.Now(), ct, cs.Gray)
+		tp.AddTimeField(time.Now(), ct, cs.Muted)
 
 		if hasNonProdVSCSTarget {
 			tp.AddField(c.VSCSTarget)
diff --git a/pkg/cmd/factory/default.go b/pkg/cmd/factory/default.go
index 923651487..598be0747 100644
--- a/pkg/cmd/factory/default.go
+++ b/pkg/cmd/factory/default.go
@@ -19,6 +19,7 @@ import (
 	"github.com/cli/cli/v2/pkg/cmd/extension"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
+	xcolor "github.com/cli/go-gh/v2/pkg/x/color"
 )
 
 var ssoHeader string
@@ -292,6 +293,8 @@ func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams {
 		io.SetPager(pager.Value)
 	}
 
+	io.SetAccessibleColorsEnabled(xcolor.IsAccessibleColorsEnabled())
+
 	return io
 }
 
diff --git a/pkg/cmd/gist/list/list.go b/pkg/cmd/gist/list/list.go
index d46d4da02..a4fd245f6 100644
--- a/pkg/cmd/gist/list/list.go
+++ b/pkg/cmd/gist/list/list.go
@@ -206,7 +206,7 @@ func printTable(io *iostreams.IOStreams, gists []shared.Gist, filter *regexp.Reg
 			tableprinter.WithColor(highlightFilesFunc(&gist)),
 		)
 		tp.AddField(visibility, tableprinter.WithColor(visColor))
-		tp.AddTimeField(time.Now(), gist.UpdatedAt, cs.Gray)
+		tp.AddTimeField(time.Now(), gist.UpdatedAt, cs.Muted)
 		tp.EndRow()
 	}
 
diff --git a/pkg/cmd/gpg-key/list/list.go b/pkg/cmd/gpg-key/list/list.go
index 9acf1d7b6..4244ba349 100644
--- a/pkg/cmd/gpg-key/list/list.go
+++ b/pkg/cmd/gpg-key/list/list.go
@@ -78,7 +78,7 @@ func listRun(opts *ListOptions) error {
 		t.AddField(gpgKey.Emails.String())
 		t.AddField(gpgKey.KeyID)
 		t.AddField(gpgKey.PublicKey, tableprinter.WithTruncate(truncateMiddle))
-		t.AddTimeField(now, gpgKey.CreatedAt, cs.Gray)
+		t.AddTimeField(now, gpgKey.CreatedAt, cs.Muted)
 		expiresAt := gpgKey.ExpiresAt.Format(time.RFC3339)
 		if t.IsTTY() {
 			if gpgKey.ExpiresAt.IsZero() {
@@ -87,7 +87,7 @@ func listRun(opts *ListOptions) error {
 				expiresAt = gpgKey.ExpiresAt.Format("2006-01-02")
 			}
 		}
-		t.AddField(expiresAt, tableprinter.WithColor(cs.Gray))
+		t.AddField(expiresAt, tableprinter.WithColor(cs.Muted))
 		t.EndRow()
 	}
 
diff --git a/pkg/cmd/pr/list/list.go b/pkg/cmd/pr/list/list.go
index ee708f161..0b86d5e11 100644
--- a/pkg/cmd/pr/list/list.go
+++ b/pkg/cmd/pr/list/list.go
@@ -224,7 +224,7 @@ func listRun(opts *ListOptions) error {
 		if !isTTY {
 			table.AddField(shared.PrStateWithDraft(&pr))
 		}
-		table.AddTimeField(opts.Now(), pr.CreatedAt, cs.Gray)
+		table.AddTimeField(opts.Now(), pr.CreatedAt, cs.Muted)
 		table.EndRow()
 	}
 	err = table.Render()
diff --git a/pkg/cmd/release/list/list.go b/pkg/cmd/release/list/list.go
index 24ec41840..dfc3ba8c1 100644
--- a/pkg/cmd/release/list/list.go
+++ b/pkg/cmd/release/list/list.go
@@ -88,7 +88,7 @@ func listRun(opts *ListOptions) error {
 	}
 
 	table := tableprinter.New(opts.IO, tableprinter.WithHeader("Title", "Type", "Tag name", "Published"))
-	iofmt := opts.IO.ColorScheme()
+	cs := opts.IO.ColorScheme()
 	for _, rel := range releases {
 		title := text.RemoveExcessiveWhitespace(rel.Name)
 		if title == "" {
@@ -100,13 +100,13 @@ func listRun(opts *ListOptions) error {
 		var badgeColor func(string) string
 		if rel.IsLatest {
 			badge = "Latest"
-			badgeColor = iofmt.Green
+			badgeColor = cs.Green
 		} else if rel.IsDraft {
 			badge = "Draft"
-			badgeColor = iofmt.Red
+			badgeColor = cs.Red
 		} else if rel.IsPrerelease {
 			badge = "Pre-release"
-			badgeColor = iofmt.Yellow
+			badgeColor = cs.Yellow
 		}
 		table.AddField(badge, tableprinter.WithColor(badgeColor))
 
@@ -116,7 +116,7 @@ func listRun(opts *ListOptions) error {
 		if rel.PublishedAt.IsZero() {
 			pubDate = rel.CreatedAt
 		}
-		table.AddTimeField(time.Now(), pubDate, iofmt.Gray)
+		table.AddTimeField(time.Now(), pubDate, cs.Muted)
 		table.EndRow()
 	}
 	err = table.Render()
diff --git a/pkg/cmd/repo/deploy-key/list/list.go b/pkg/cmd/repo/deploy-key/list/list.go
index 0cf2861c1..79d00c912 100644
--- a/pkg/cmd/repo/deploy-key/list/list.go
+++ b/pkg/cmd/repo/deploy-key/list/list.go
@@ -90,7 +90,7 @@ func listRun(opts *ListOptions) error {
 		}
 		t.AddField(sshType)
 		t.AddField(deployKey.Key, tableprinter.WithTruncate(truncateMiddle))
-		t.AddTimeField(now, deployKey.CreatedAt, cs.Gray)
+		t.AddTimeField(now, deployKey.CreatedAt, cs.Muted)
 		t.EndRow()
 	}
 
diff --git a/pkg/cmd/repo/list/list.go b/pkg/cmd/repo/list/list.go
index 2cee2c480..116439290 100644
--- a/pkg/cmd/repo/list/list.go
+++ b/pkg/cmd/repo/list/list.go
@@ -181,7 +181,7 @@ func listRun(opts *ListOptions) error {
 	totalMatchCount := len(listResult.Repositories)
 	for _, repo := range listResult.Repositories {
 		info := repoInfo(repo)
-		infoColor := cs.Gray
+		infoColor := cs.Muted
 
 		if repo.IsPrivate {
 			infoColor = cs.Yellow
@@ -195,7 +195,7 @@ func listRun(opts *ListOptions) error {
 		tp.AddField(repo.NameWithOwner, tableprinter.WithColor(cs.Bold))
 		tp.AddField(text.RemoveExcessiveWhitespace(repo.Description))
 		tp.AddField(info, tableprinter.WithColor(infoColor))
-		tp.AddTimeField(opts.Now(), *t, cs.Gray)
+		tp.AddTimeField(opts.Now(), *t, cs.Muted)
 		tp.EndRow()
 	}
 
diff --git a/pkg/cmd/run/list/list.go b/pkg/cmd/run/list/list.go
index 7b18c391d..7128ae253 100644
--- a/pkg/cmd/run/list/list.go
+++ b/pkg/cmd/run/list/list.go
@@ -176,7 +176,7 @@ func listRun(opts *ListOptions) error {
 		tp.AddField(string(run.Event))
 		tp.AddField(fmt.Sprintf("%d", run.ID), tableprinter.WithColor(cs.Cyan))
 		tp.AddField(run.Duration(opts.now).String())
-		tp.AddTimeField(opts.now, run.StartedTime(), cs.Gray)
+		tp.AddTimeField(opts.now, run.StartedTime(), cs.Muted)
 		tp.EndRow()
 	}
 
diff --git a/pkg/cmd/ssh-key/list/list.go b/pkg/cmd/ssh-key/list/list.go
index eebc82f90..f69e57ea3 100644
--- a/pkg/cmd/ssh-key/list/list.go
+++ b/pkg/cmd/ssh-key/list/list.go
@@ -89,11 +89,11 @@ func listRun(opts *ListOptions) error {
 			t.AddField(id)
 			t.AddField(sshKey.Key, tableprinter.WithTruncate(truncateMiddle))
 			t.AddField(sshKey.Type)
-			t.AddTimeField(now, sshKey.CreatedAt, cs.Gray)
+			t.AddTimeField(now, sshKey.CreatedAt, cs.Muted)
 		} else {
 			t.AddField(sshKey.Title)
 			t.AddField(sshKey.Key)
-			t.AddTimeField(now, sshKey.CreatedAt, cs.Gray)
+			t.AddTimeField(now, sshKey.CreatedAt, cs.Muted)
 			t.AddField(id)
 			t.AddField(sshKey.Type)
 		}
diff --git a/pkg/iostreams/color.go b/pkg/iostreams/color.go
index 07fdcd79f..58d13e6ef 100644
--- a/pkg/iostreams/color.go
+++ b/pkg/iostreams/color.go
@@ -30,32 +30,36 @@ var (
 	greenBold             = ansi.ColorFunc("green+b")
 	highlightStart        = ansi.ColorCode(highlightStyle)
 	highlight             = ansi.ColorFunc(highlightStyle)
+	darkThemeMuted        = ansi.ColorFunc("white+d")
 	darkThemeTableHeader  = ansi.ColorFunc("white+du")
+	lightThemeMuted       = ansi.ColorFunc("black+h")
 	lightThemeTableHeader = ansi.ColorFunc("black+hu")
 	noThemeTableHeader    = ansi.ColorFunc("default+u")
 
 	gray256 = func(t string) string {
-		return fmt.Sprintf("\x1b[%d;5;%dm%s\x1b[m", 38, 242, t)
+		return fmt.Sprintf("\x1b[%d;5;%dm%s\x1b[0m", 38, 242, t)
 	}
 )
 
 // 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 bool, theme string) *ColorScheme {
+func NewColorScheme(enabled, is256enabled, trueColor, accessibleColors bool, theme string) *ColorScheme {
 	return &ColorScheme{
-		enabled:      enabled,
-		is256enabled: is256enabled,
-		hasTrueColor: trueColor,
-		theme:        theme,
+		enabled:          enabled,
+		is256enabled:     is256enabled,
+		hasTrueColor:     trueColor,
+		accessibleColors: accessibleColors,
+		theme:            theme,
 	}
 }
 
 type ColorScheme struct {
-	enabled      bool
-	is256enabled bool
-	hasTrueColor bool
-	theme        string
+	enabled          bool
+	is256enabled     bool
+	hasTrueColor     bool
+	accessibleColors bool
+	theme            string
 }
 
 func (c *ColorScheme) Enabled() bool {
@@ -73,6 +77,31 @@ func (c *ColorScheme) Boldf(t string, args ...interface{}) string {
 	return c.Bold(fmt.Sprintf(t, args...))
 }
 
+func (c *ColorScheme) Muted(t string) string {
+	// Fallback to previous logic if accessible colors preview is disabled.
+	if !c.accessibleColors {
+		return c.Gray(t)
+	}
+
+	// Muted text is only stylized if color is enabled.
+	if !c.enabled {
+		return t
+	}
+
+	switch c.theme {
+	case LightTheme:
+		return lightThemeMuted(t)
+	case DarkTheme:
+		return darkThemeMuted(t)
+	default:
+		return t
+	}
+}
+
+func (c *ColorScheme) Mutedf(t string, args ...interface{}) string {
+	return c.Muted(fmt.Sprintf(t, args...))
+}
+
 func (c *ColorScheme) Red(t string) string {
 	if !c.enabled {
 		return t
@@ -113,6 +142,7 @@ func (c *ColorScheme) GreenBold(t string) string {
 	return greenBold(t)
 }
 
+// Use Muted instead for thematically contrasting color.
 func (c *ColorScheme) Gray(t string) string {
 	if !c.enabled {
 		return t
@@ -123,6 +153,7 @@ func (c *ColorScheme) Gray(t string) string {
 	return gray(t)
 }
 
+// Use Mutedf instead for thematically contrasting color.
 func (c *ColorScheme) Grayf(t string, args ...interface{}) string {
 	return c.Gray(fmt.Sprintf(t, args...))
 }
diff --git a/pkg/iostreams/iostreams.go b/pkg/iostreams/iostreams.go
index 6c12d7911..30981386b 100644
--- a/pkg/iostreams/iostreams.go
+++ b/pkg/iostreams/iostreams.go
@@ -70,8 +70,9 @@ type IOStreams struct {
 	stderrTTYOverride bool
 	stderrIsTTY       bool
 
-	colorOverride bool
-	colorEnabled  bool
+	colorOverride           bool
+	colorEnabled            bool
+	accessibleColorsEnabled bool
 
 	pagerCommand string
 	pagerProcess *os.Process
@@ -366,7 +367,7 @@ func (s *IOStreams) TerminalWidth() int {
 }
 
 func (s *IOStreams) ColorScheme() *ColorScheme {
-	return NewColorScheme(s.ColorEnabled(), s.ColorSupport256(), s.HasTrueColor(), s.TerminalTheme())
+	return NewColorScheme(s.ColorEnabled(), s.ColorSupport256(), s.HasTrueColor(), s.AccessibleColorsEnabled(), s.TerminalTheme())
 }
 
 func (s *IOStreams) ReadUserFile(fn string) ([]byte, error) {
@@ -391,6 +392,14 @@ func (s *IOStreams) TempFile(dir, pattern string) (*os.File, error) {
 	return os.CreateTemp(dir, pattern)
 }
 
+func (s *IOStreams) SetAccessibleColorsEnabled(enabled bool) {
+	s.accessibleColorsEnabled = enabled
+}
+
+func (s *IOStreams) AccessibleColorsEnabled() bool {
+	return s.accessibleColorsEnabled
+}
+
 func System() *IOStreams {
 	terminal := ghTerm.FromEnv()
 


Test patch:
diff --git a/pkg/cmd/attestation/verify/policy.go b/pkg/cmd/attestation/verify/policy.go
index 1d1595eca..1060a781e 100644
--- a/pkg/cmd/attestation/verify/policy.go
+++ b/pkg/cmd/attestation/verify/policy.go
@@ -161,7 +161,7 @@ func validateSignerWorkflow(hostname, signerWorkflow string) (string, error) {
 	// if the provided workflow did not match the expect format
 	// we move onto creating a signer workflow using the provided host name
 	if hostname == "" {
-		return "", errors.New("unknown signer workflow host")
+		return "", errors.New("unknown host")
 	}
 
 	return fmt.Sprintf("^https://%s/%s", hostname, signerWorkflow), nil
diff --git a/pkg/cmd/attestation/verify/policy_test.go b/pkg/cmd/attestation/verify/policy_test.go
index ff10cad11..d376498b6 100644
--- a/pkg/cmd/attestation/verify/policy_test.go
+++ b/pkg/cmd/attestation/verify/policy_test.go
@@ -275,7 +275,7 @@ func TestValidateSignerWorkflow(t *testing.T) {
 			name:                   "workflow with no host specified",
 			providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml",
 			expectErr:              true,
-			errContains:            "unknown signer workflow host",
+			errContains:            "unknown host",
 		},
 		{
 			name:                   "workflow with default host",
diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go
index 3affdfabb..65ae8ca3e 100644
--- a/pkg/cmd/attestation/verify/verify.go
+++ b/pkg/cmd/attestation/verify/verify.go
@@ -30,107 +30,58 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
 			Verify the integrity and provenance of an artifact using its associated
 			cryptographically signed attestations.
 
-			## Understanding Verification
+			In order to verify an attestation, you must validate the identity of the Actions
+			workflow that produced the attestation (a.k.a. the signer workflow). Given this
+			identity, the verification process checks the signatures in the attestations,
+			and confirms that the attestation refers to provided artifact.
 
-			An attestation is a claim (i.e. a provenance statement) made by an actor
-			(i.e. a GitHub Actions workflow) regarding a subject (i.e. an artifact).
-
-			In order to verify an attestation, you must provide an artifact and validate:
-			* the identity of the actor that produced the attestation
-			* the expected attestation predicate type (the nature of the claim)
-
-			By default, this command enforces the %[1]s%[2]s%[1]s
-			predicate type. To verify other attestation predicate types use the
-			%[1]s--predicate-type%[1]s flag.
-
-			The "actor identity" consists of:
-			* the repository or the repository owner the artifact is linked with
-			* the Actions workflow that produced the attestation (a.k.a the
-			  signer workflow)
-
-			This identity is then validated against the attestation's certificate's
-			SourceRepository, SourceRepositoryOwner, and SubjectAlternativeName
-			(SAN) fields, among others.
-
-			It is up to you to decide how precisely you want to enforce this identity.
-
-			At a minimum, this command requires either:
-			* the %[1]s--owner%[1]s flag (e.g. --owner github), or
-			* the %[1]s--repo%[1]s flag (e.g. --repo github/example)
-
-			The more precisely you specify the identity, the more control you will
-			have over the security guarantees offered by the verification process.
-
-			Ideally, the path of the signer workflow is also validated using the
-			%[1]s--signer-workflow%[1]s or %[1]s--cert-identity%[1]s flags.
-
-			Please note: if your attestation was generated via a reusable workflow then
-			that reusable workflow is the signer whose identity needs to be validated.
-			In this situation, you must use either the %[1]s--signer-workflow%[1]s or
-			the %[1]s--signer-repo%[1]s flag.
-
-			For more options, see the other available flags.
-
-			## Loading Artifacts And Attestations
-
-			To specify the artifact, this command requires:
+			To specify the artifact, the command requires:
 			* a file path to an artifact, or
 			* a container image URI (e.g. %[1]soci://<image-uri>%[1]s)
 			  * (note that if you provide an OCI URL, you must already be authenticated with
 			its container registry)
 
-			By default, this command will attempt to fetch relevant attestations via the
-			GitHub API using the values provided to %[1]s--owner%[1]s or  %[1]s--repo%[1]s.
-
-			To instead fetch attestations from your artifact's OCI registry, use the
-			%[1]s--bundle-from-oci%[1]s flag.
-
-			For offline verification using attestations stored on disk (c.f. the download command)
-			provide a path to the %[1]s--bundle%[1]s flag.
+			To fetch the attestation, and validate the identity of the signer, the command
+			requires either:
+			* the %[1]s--repo%[1]s flag (e.g. --repo github/example).
+			* the %[1]s--owner%[1]s flag (e.g. --owner github), or
 
-			## Additional Policy Enforcement
+			The %[1]s--repo%[1]s flag value must match the name of the GitHub repository
+			that the artifact is linked with.
 
-			Given the %[1]s--format=json%[1]s flag, upon successful verification this
-			command will output a JSON array containing one entry per verified attestation.
+			The %[1]s--owner%[1]s flag value must match the name of the GitHub organization
+			that the artifact's linked repository belongs to.
 
-			This output can then be used for additional policy enforcement, i.e. by being
-			piped into a policy engine.
+			By default, the verify command will:
+			- only verify provenance attestations
+			- attempt to fetch relevant attestations via the GitHub API.
 
-			Each object in the array contains two properties:
-			* an %[1]sattestation%[1]s object, which contains the bundle that was verified
-			* a %[1]sverificationResult%[1]s object, which is a parsed representation of the
-			  contents of the bundle that was verified.
+			To verify other types of attestations, use the %[1]s--predicate-type%[1]s flag.
 
-			Within the %[1]sverificationResult%[1]s object you will find:
-			* %[1]ssignature.certificate%[1]s, which is a parsed representation of the X.509
-			  certificate embedded in the attestation,
-			* %[1]sverifiedTimestamps%[1]s, an array of objects denoting when the attestation
-			  was witnessed by a transparency log or a timestamp authority
-			* %[1]sstatement%[1]s, which contains the %[1]ssubject%[1]s array referencing artifacts,
-			  the %[1]spredicateType%[1]s field, and the %[1]spredicate%[1]s object which contains
-			  additional, often user-controllable, metadata
+			To use your artifact's OCI registry instead of GitHub's API, use the
+			%[1]s--bundle-from-oci%[1]s flag. For offline verification, using attestations
+			stored on desk (c.f. the download command), provide a path to the %[1]s--bundle%[1]s flag.
 
-			IMPORTANT: please note that only the %[1]ssignature.certificate%[1]s and the
-			%[1]sverifiedTimestamps%[1]s properties contain values that cannot be
-			manipulated by the workflow that originated the attestation.
+			To see the full results that are generated upon successful verification, i.e.
+			for use with a policy engine, provide the %[1]s--format=json%[1]s flag.
 
-			When dealing with attestations created within GitHub Actions, the contents of
-			%[1]ssignature.certificate%[1]s are populated directly from the OpenID Connect
-			token that GitHub has generated. The contents of the %[1]sverifiedTimestamps%[1]s
-			array are populated from the signed timestamps originating from either a
-			transparency log or a timestamp authority – and likewise cannot be forged by users.
+			The signer workflow's identity is validated against the Subject Alternative Name (SAN)
+			within the attestation certificate. Often, the signer workflow is the
+			same workflow that started the run and generated the attestation, and will be
+			located inside your repository. For this reason, by default this command uses
+			either the %[1]s--repo%[1]s or the %[1]s--owner%[1]s flag value to validate the SAN.
 
-			When designing policy enforcement using this output, special care must be taken
-			when examining the contents of the %[1]sstatement.predicate%[1]s property:
-			should an attacker gain access to your workflow's execution context, they
-			could then falsify the contents of the %[1]sstatement.predicate%[1]s.
+			However, sometimes the caller workflow is not the same workflow that
+			performed the signing. If your attestation was generated via a reusable
+			workflow, then that reusable workflow is the signer whose identity needs to be
+			validated. In this situation, the signer workflow may or may not be located
+			inside your %[1]s--repo%[1]s or %[1]s--owner%[1]s.
 
-			To mitigate this attack vector, consider using a "trusted builder": when generating
-			an artifact, have the build and attestation signing occur within a reusable workflow
-			whose execution cannot be influenced by input provided through the caller workflow.
+			When using reusable workflows, use the %[1]s--signer-repo%[1]s, %[1]s--signer-workflow%[1]s,
+			or %[1]s--cert-identity%[1]s flags to validate the signer workflow's identity.
 
-			See above re: %[1]s--signer-workflow%[1]s.
-			`, "`", verification.SLSAPredicateV1),
+			For more policy verification options, see the other available flags.
+			`, "`"),
 		Example: heredoc.Doc(`
 			# Verify an artifact linked with a repository
 			$ gh attestation verify example.bin --repo github/example
@@ -230,23 +181,23 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
 	verifyCmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository name in the format <owner>/<repo>")
 	verifyCmd.MarkFlagsMutuallyExclusive("owner", "repo")
 	verifyCmd.MarkFlagsOneRequired("owner", "repo")
+	verifyCmd.Flags().StringVarP(&opts.PredicateType, "predicate-type", "", verification.SLSAPredicateV1, "Filter attestations by provided predicate type")
 	verifyCmd.Flags().BoolVarP(&opts.NoPublicGood, "no-public-good", "", false, "Do not verify attestations signed with Sigstore public good instance")
 	verifyCmd.Flags().StringVarP(&opts.TrustedRoot, "custom-trusted-root", "", "", "Path to a trusted_root.jsonl file; likely for offline verification")
 	verifyCmd.Flags().IntVarP(&opts.Limit, "limit", "L", api.DefaultLimit, "Maximum number of attestations to fetch")
 	cmdutil.AddFormatFlags(verifyCmd, &opts.exporter)
-	verifyCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use")
 	// policy enforcement flags
-	verifyCmd.Flags().StringVarP(&opts.PredicateType, "predicate-type", "", verification.SLSAPredicateV1, "Enforce that verified attestations' predicate type matches the provided value")
 	verifyCmd.Flags().BoolVarP(&opts.DenySelfHostedRunner, "deny-self-hosted-runners", "", false, "Fail verification for attestations generated on self-hosted runners")
-	verifyCmd.Flags().StringVarP(&opts.SAN, "cert-identity", "", "", "Enforce that the certificate's SubjectAlternativeName matches the provided value exactly")
-	verifyCmd.Flags().StringVarP(&opts.SANRegex, "cert-identity-regex", "i", "", "Enforce that the certificate's SubjectAlternativeName matches the provided regex")
-	verifyCmd.Flags().StringVarP(&opts.SignerRepo, "signer-repo", "", "", "Enforce that the workflow that signed the attestation's repository matches the provided value (<owner>/<repo>)")
-	verifyCmd.Flags().StringVarP(&opts.SignerWorkflow, "signer-workflow", "", "", "Enforce that the workflow that signed the attestation matches the provided value ([host/]<owner>/<repo>/<path>/<to>/<workflow>)")
+	verifyCmd.Flags().StringVarP(&opts.SAN, "cert-identity", "", "", "Enforce that the certificate's subject alternative name matches the provided value exactly")
+	verifyCmd.Flags().StringVarP(&opts.SANRegex, "cert-identity-regex", "i", "", "Enforce that the certificate's subject alternative name matches the provided regex")
+	verifyCmd.Flags().StringVarP(&opts.SignerRepo, "signer-repo", "", "", "Repository of reusable workflow that signed attestation in the format <owner>/<repo>")
+	verifyCmd.Flags().StringVarP(&opts.SignerWorkflow, "signer-workflow", "", "", "Workflow that signed attestation in the format [host/]<owner>/<repo>/<path>/<to>/<workflow>")
 	verifyCmd.MarkFlagsMutuallyExclusive("cert-identity", "cert-identity-regex", "signer-repo", "signer-workflow")
-	verifyCmd.Flags().StringVarP(&opts.OIDCIssuer, "cert-oidc-issuer", "", verification.GitHubOIDCIssuer, "Enforce that the issuer of the OIDC token matches the provided value")
-	verifyCmd.Flags().StringVarP(&opts.SignerDigest, "signer-digest", "", "", "Enforce that the digest associated with the signer workflow matches the provided value")
-	verifyCmd.Flags().StringVarP(&opts.SourceRef, "source-ref", "", "", "Enforce that the git ref associated with the source repository matches the provided value")
-	verifyCmd.Flags().StringVarP(&opts.SourceDigest, "source-digest", "", "", "Enforce that the digest associated with the source repository matches the provided value")
+	verifyCmd.Flags().StringVarP(&opts.OIDCIssuer, "cert-oidc-issuer", "", verification.GitHubOIDCIssuer, "Issuer of the OIDC token")
+	verifyCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use")
+	verifyCmd.Flags().StringVarP(&opts.SignerDigest, "signer-digest", "", "", "Digest associated with the signer workflow")
+	verifyCmd.Flags().StringVarP(&opts.SourceRef, "source-ref", "", "", "Ref associated with the source workflow")
+	verifyCmd.Flags().StringVarP(&opts.SourceDigest, "source-digest", "", "", "Digest associated with the source workflow")
 
 	return verifyCmd
 }
diff --git a/pkg/cmd/gist/list/list_test.go b/pkg/cmd/gist/list/list_test.go
index 4f6c8a9f7..ad6d8cdb5 100644
--- a/pkg/cmd/gist/list/list_test.go
+++ b/pkg/cmd/gist/list/list_test.go
@@ -487,8 +487,8 @@ func Test_listRun(t *testing.T) {
 			},
 			wantOut: heredoc.Docf(`
 				%[1]s[0;4;39mID  %[1]s[0m  %[1]s[0;4;39mDESCRIPTION                  %[1]s[0m  %[1]s[0;4;39mFILES  %[1]s[0m  %[1]s[0;4;39mVISIBILITY%[1]s[0m  %[1]s[0;4;39mUPDATED          %[1]s[0m
-				1234  %[1]s[0;30;43mocto%[1]s[0m%[1]s[0;1;39m match in the description%[1]s[0m  1 file   %[1]s[0;32mpublic    %[1]s[0m  %[1]s[38;5;242mabout 6 hours ago%[1]s[m
-				2345  %[1]s[0;1;39mmatch in the file name       %[1]s[0m  %[1]s[0;30;43m2 files%[1]s[0m  %[1]s[0;31msecret    %[1]s[0m  %[1]s[38;5;242mabout 6 hours ago%[1]s[m
+				1234  %[1]s[0;30;43mocto%[1]s[0m%[1]s[0;1;39m match in the description%[1]s[0m  1 file   %[1]s[0;32mpublic    %[1]s[0m  %[1]s[38;5;242mabout 6 hours ago%[1]s[0m
+				2345  %[1]s[0;1;39mmatch in the file name       %[1]s[0m  %[1]s[0;30;43m2 files%[1]s[0m  %[1]s[0;31msecret    %[1]s[0m  %[1]s[38;5;242mabout 6 hours ago%[1]s[0m
 			`, "\x1b"),
 		},
 		{
@@ -694,7 +694,7 @@ func Test_highlightMatch(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			cs := iostreams.NewColorScheme(tt.color, false, false, iostreams.NoTheme)
+			cs := iostreams.NewColorScheme(tt.color, false, false, false, iostreams.NoTheme)
 
 			matched := false
 			got, err := highlightMatch(tt.input, regex, &matched, cs.Blue, cs.Highlight)
diff --git a/pkg/iostreams/color_test.go b/pkg/iostreams/color_test.go
index b35c2eb73..b0cf994e3 100644
--- a/pkg/iostreams/color_test.go
+++ b/pkg/iostreams/color_test.go
@@ -20,28 +20,28 @@ func TestColorFromRGB(t *testing.T) {
 			hex:   "fc0303",
 			text:  "red",
 			wants: "\033[38;2;252;3;3mred\033[0m",
-			cs:    NewColorScheme(true, true, true, NoTheme),
+			cs:    NewColorScheme(true, true, true, false, NoTheme),
 		},
 		{
 			name:  "no truecolor",
 			hex:   "fc0303",
 			text:  "red",
 			wants: "red",
-			cs:    NewColorScheme(true, true, false, NoTheme),
+			cs:    NewColorScheme(true, true, false, false, NoTheme),
 		},
 		{
 			name:  "no color",
 			hex:   "fc0303",
 			text:  "red",
 			wants: "red",
-			cs:    NewColorScheme(false, false, false, NoTheme),
+			cs:    NewColorScheme(false, false, false, false, NoTheme),
 		},
 		{
 			name:  "invalid hex",
 			hex:   "fc0",
 			text:  "red",
 			wants: "red",
-			cs:    NewColorScheme(false, false, false, NoTheme),
+			cs:    NewColorScheme(false, false, false, false, NoTheme),
 		},
 	}
 
@@ -64,28 +64,28 @@ func TestHexToRGB(t *testing.T) {
 			hex:   "fc0303",
 			text:  "red",
 			wants: "\033[38;2;252;3;3mred\033[0m",
-			cs:    NewColorScheme(true, true, true, NoTheme),
+			cs:    NewColorScheme(true, true, true, false, NoTheme),
 		},
 		{
 			name:  "no truecolor",
 			hex:   "fc0303",
 			text:  "red",
 			wants: "red",
-			cs:    NewColorScheme(true, true, false, NoTheme),
+			cs:    NewColorScheme(true, true, false, false, NoTheme),
 		},
 		{
 			name:  "no color",
 			hex:   "fc0303",
 			text:  "red",
 			wants: "red",
-			cs:    NewColorScheme(false, false, false, NoTheme),
+			cs:    NewColorScheme(false, false, false, false, NoTheme),
 		},
 		{
 			name:  "invalid hex",
 			hex:   "fc0",
 			text:  "red",
 			wants: "red",
-			cs:    NewColorScheme(false, false, false, NoTheme),
+			cs:    NewColorScheme(false, false, false, false, NoTheme),
 		},
 	}
 
@@ -109,61 +109,61 @@ func TestTableHeader(t *testing.T) {
 	}{
 		{
 			name:     "when color is disabled, text is not stylized",
-			cs:       NewColorScheme(false, false, false, NoTheme),
+			cs:       NewColorScheme(false, false, false, true, 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, NoTheme),
+			cs:       NewColorScheme(true, false, false, true, 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, LightTheme),
+			cs:       NewColorScheme(true, false, false, true, 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, DarkTheme),
+			cs:       NewColorScheme(true, false, false, true, 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, NoTheme),
+			cs:       NewColorScheme(true, true, false, true, 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, LightTheme),
+			cs:       NewColorScheme(true, true, false, true, 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, DarkTheme),
+			cs:       NewColorScheme(true, true, false, true, 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, NoTheme),
+			cs:       NewColorScheme(true, true, true, true, 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, LightTheme),
+			cs:       NewColorScheme(true, true, true, true, 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, DarkTheme),
+			cs:       NewColorScheme(true, true, true, true, DarkTheme),
 			input:    "this should have light foreground color and underlined",
 			expected: fmt.Sprintf("%sthis should have light foreground color and underlined%s", dimBlackUnderline, reset),
 		},
@@ -175,3 +175,67 @@ func TestTableHeader(t *testing.T) {
 		})
 	}
 }
+
+func TestMuted(t *testing.T) {
+	reset := "\x1b[0m"
+	gray4bit := "\x1b[0;90m"
+	gray8bit := "\x1b[38;5;242m"
+	brightBlack4bit := "\x1b[0;90m"
+	dimBlack4bit := "\x1b[0;2;37m"
+
+	tests := []struct {
+		name     string
+		cs       *ColorScheme
+		input    string
+		expected string
+	}{
+		{
+			name:     "when color is disabled but accessible colors are disabled, text is not stylized",
+			cs:       NewColorScheme(false, false, false, false, NoTheme),
+			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),
+			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),
+			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),
+			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),
+			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),
+			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),
+			input:    "this should have no explicit color",
+			expected: "this should have no explicit color",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.expected, tt.cs.Muted(tt.input))
+		})
+	}
+}