Repository: cli/go-gh
Pull request #186: Introduce experimental accessible colors in markdown rendering
Author: login: andyfeller
Created: 2025-03-10T14:17:04Z | Merged: 2025-03-25T11:54:29Z

Relates github/cli#830

These changes relate to a new experimental feature for rendering markdown in GitHub CLI and CLI extensions using accessible colors.

Out of the box, `markdown` package continues to render GFM using existing `charmbracelet/glamour` light and dark styles in 8-bit color.

By setting `accessible_colors` configuration setting or `GH_ACCESSIBLE_COLORS` environment variable, `markdown` package will begin using ANSI 4-bit colors that most modern terminals allow users to customize.  This will allow users with low vision and color blindness to tailor their experiences more effectively.

## Go version change

This pull request bumps the minimum Go version from 1.21 to 1.23 due to transitive dependencies from experimental Go module:

- https://github.com/charmbracelet/glamour/pull/396
- https://github.com/charmbracelet/glamour/pull/397

## Demo

The following accessible colors have been aligned for consistency with [Primer Design System](https://primer.style/) within the limited palette of 4-bit colors.

### Headers

**Current default**

<img width="1512" alt="headers, current release, dark appearance" src="https://github.com/user-attachments/assets/fd02f752-a9c4-4a65-b554-52e101ed6a16" />

<img width="1512" alt="headers, current release, light apperance" src="https://github.com/user-attachments/assets/1135363b-59c7-4f3a-8e43-315ffaef6609" />

**Accessible**

<img width="1512" alt="headers, accessible-colors, dark appearance" src="https://github.com/user-attachments/assets/143dd4ae-322e-4a96-bd8d-c59d48b51863" />

<img width="1512" alt="headers, accessible-colors, light apperance" src="https://github.com/user-attachments/assets/8908f27c-0ff2-4d1d-b38f-bcd0abd3fe25" />

**New default**

<img width="1512" alt="headers, new default, dark appearance" src="https://github.com/user-attachments/assets/a34ad629-2532-4483-821b-3289776f5e45" />

<img width="1512" alt="headers, new default, light appearance" src="https://github.com/user-attachments/assets/3aff1e6e-8517-4534-b357-06b422c7429d" />


### Horizontal Rule

**Current default**

<img width="1512" alt="horizontal rule, current release, dark appearance" src="https://github.com/user-attachments/assets/2082fb6e-b9ad-4fc7-895d-b87686bb9e45" />

<img width="1512" alt="horizontal rule, current release, light appearance" src="https://github.com/user-attachments/assets/e07c59cf-7732-46ad-88fd-c354b9f70e24" />

**Accessible**

<img width="1512" alt="horizontal rule, accessible-colors, dark appearance" src="https://github.com/user-attachments/assets/e6cf9233-8732-4a53-a6b5-d10d6f7d7548" />

<img width="1512" alt="horizontal rule, accessible-colors, light appearance" src="https://github.com/user-attachments/assets/c87db5e2-48f3-4a5a-8057-0ae639beaf10" />

**New default**

<img width="1512" alt="horizontal rule, new default, dark appearance" src="https://github.com/user-attachments/assets/1d12e9d9-a4e5-455f-9093-4547d3b6efa9" />

<img width="1512" alt="horizontal rule, new default, light appearance" src="https://github.com/user-attachments/assets/f686636d-ce15-4164-8464-1ac385b2ea2a" />


### Images

**Current default**

<img width="1512" alt="image, current release, dark appearance" src="https://github.com/user-attachments/assets/beb0df04-eaa8-466b-8370-fd02b5b09bde" />

<img width="1512" alt="image, current release, light appearance" src="https://github.com/user-attachments/assets/d2a875f9-df95-4c59-a85e-3d954e250822" />

**Accessible**

<img width="1512" alt="image, accessible-colors, dark appearance" src="https://github.com/user-attachments/assets/feff5ad6-4a85-47bc-8f5f-1dc3e165262e" />

<img width="1512" alt="image, accessible-colors, light appearance" src="https://github.com/user-attachments/assets/71821ac8-23b7-4872-a414-89eb36a71558" />

**New default**

<img width="1512" alt="image, new default, dark appearance" src="https://github.com/user-attachments/assets/6d2b3bdd-8b5f-44c2-ada7-583aca7547e6" />

<img width="1512" alt="image, new default, light appearance" src="https://github.com/user-attachments/assets/8306a2f0-6e69-47c3-9bbe-4ba5b3bb26f9" />


### Links

**Current default**

<img width="1512" alt="link, current release, dark appearance" src="https://github.com/user-attachments/assets/3cdb0c25-6437-4b47-9cb4-99b379f07af1" />

<img width="1512" alt="link, current release, light appearance" src="https://github.com/user-attachments/assets/c3545d71-1aed-426e-8e12-bd160c7d4657" />

**Accessible**

<img width="1512" alt="link, accessible-colors, dark appearance" src="https://github.com/user-attachments/assets/d859a5fd-4703-4835-ad01-8f5a64348190" />

<img width="1512" alt="link, accessible-colors, light appearance" src="https://github.com/user-attachments/assets/3c01d937-19d9-4d23-8dc1-bca65b4a03b2" />

**New default**

<img width="1512" alt="link, new default, dark appearance" src="https://github.com/user-attachments/assets/7e5b6856-36ef-452b-b7af-6638fe3310ce" />

<img width="1512" alt="link, new default, light appearance" src="https://github.com/user-attachments/assets/9cd3ff3b-e0b7-4767-9449-2da0ce6c2fce" />


### Code

**Current default**

<img width="1512" alt="code, current release, dark appearance" src="https://github.com/user-attachments/assets/eba8f35d-58d0-48a1-bfe3-0fefd56a3e25" />

<img width="1512" alt="code, current release, light appearance" src="https://github.com/user-attachments/assets/e0dd4420-3c20-4993-9cb2-82990dc57afe" />

**Accessible**

<img width="1512" alt="code, accessible-colors, dark appearance" src="https://github.com/user-attachments/assets/d7864d5d-451e-429d-aa4d-310286aaa24c" />

<img width="1512" alt="code, accessible-colors, light appearance" src="https://github.com/user-attachments/assets/8dde51ea-d07c-4f87-9487-76a06d9c0f83" />

**New default**

<img width="1512" alt="code, new default, dark appearance" src="https://github.com/user-attachments/assets/bdf63daf-adcb-4bae-a6f8-8a5345c7d068" />

<img width="1512" alt="code, new default, light appearance" src="https://github.com/user-attachments/assets/792e411b-0f7f-45ce-b5e5-fef706ef5633" />


### Quotes

**Current default**

<img width="1512" alt="quotes, current release, dark appearance" src="https://github.com/user-attachments/assets/2adb0329-9852-4ae4-a9d4-4b64ec5fc0e0" />

<img width="1512" alt="quotes, current release, light appearance" src="https://github.com/user-attachments/assets/9d68bdc5-f75d-4e12-8d04-d368cd312c2c" />

**Accessible**

<img width="1512" alt="quotes, accessible-colors, dark appearance" src="https://github.com/user-attachments/assets/ed100a5e-abcf-4df8-a691-4e15da7f8941" />

<img width="1512" alt="quotes, accessible-colors, light appearance" src="https://github.com/user-attachments/assets/3bdffc9e-5412-489c-a9d9-217c36a0a668" />

**New default**

<img width="1512" alt="quotes, new default, dark appearance" src="https://github.com/user-attachments/assets/c39a60b9-ad55-4186-a44d-fbe24dd0b3a4" />

<img width="1512" alt="quotes, new default, light appearance" src="https://github.com/user-attachments/assets/ea9ff9cc-adc6-49ad-8d99-d9383cece902" />


### Tables

**Current default**

<img width="1512" alt="table, current release, dark appearance" src="https://github.com/user-attachments/assets/79fa72cb-1c9e-432e-942f-27517980ab68" />

<img width="1512" alt="table, current release, light appearance" src="https://github.com/user-attachments/assets/0c88b474-212d-4764-b655-55cba7c7ac62" />

**Accessible**

<img width="1512" alt="table, accessible-colors, dark appearance" src="https://github.com/user-attachments/assets/b429b0a7-722e-405c-bbd5-990552cee333" />

<img width="1512" alt="table, accessible-colors, light appearance" src="https://github.com/user-attachments/assets/8644a80a-a171-437c-a7f4-d991950a72e5" />

**New default**

<img width="1512" alt="table, new default, dark appearance" src="https://github.com/user-attachments/assets/16932cfd-3209-470b-9790-3f791cf7bb68" />

<img width="1512" alt="table, new default, light appearance" src="https://github.com/user-attachments/assets/e679c176-9972-4f4f-8b63-6374b5131aab" />



Diff:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 0378328..66aaefa 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -7,20 +7,21 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - name: Set up Go
-        uses: actions/setup-go@v3
-        with:
-          go-version: "1.21"
-
       - name: Checkout repository
         uses: actions/checkout@v4
 
+      - name: Set up Go
+        uses: actions/setup-go@v5
+        with:
+          go-version-file: go.mod
+
       - name: Check dependencies
         run: |
           go mod tidy
           git diff --exit-code go.mod
 
       - name: Lint
-        uses: golangci/golangci-lint-action@v3.4.0
+        uses: golangci/golangci-lint-action@v6
         with:
-          version: v1.54
+          version: v1.64
+          problem-matchers: true
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 072eec2..f868547 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -8,18 +8,17 @@ jobs:
       fail-fast: false
       matrix:
         os: [ubuntu-latest, windows-latest, macos-latest]
-        go: ["1.21"]
 
     runs-on: ${{ matrix.os }}
 
     steps:
-      - name: Set up Go
-        uses: actions/setup-go@v3
-        with:
-          go-version: ${{ matrix.go }}
-
       - name: Checkout repository
         uses: actions/checkout@v4
 
+      - name: Set up Go
+        uses: actions/setup-go@v5
+        with:
+          go-version-file: go.mod
+
       - name: Run tests
         run: go test -v ./...
diff --git a/go.mod b/go.mod
index 0ace715..e637eaa 100644
--- a/go.mod
+++ b/go.mod
@@ -1,27 +1,29 @@
 module github.com/cli/go-gh/v2
 
-go 1.21
+go 1.23.0
 
 require (
 	github.com/AlecAivazis/survey/v2 v2.3.7
 	github.com/MakeNowJust/heredoc v1.0.0
 	github.com/Masterminds/sprig/v3 v3.3.0
-	github.com/charmbracelet/glamour v0.7.0
-	github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c
+	github.com/alecthomas/chroma/v2 v2.14.0
+	github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3
+	github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc
 	github.com/cli/browser v1.3.0
 	github.com/cli/safeexec v1.0.0
 	github.com/cli/shurcooL-graphql v0.0.4
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
 	github.com/henvic/httpretty v0.0.6
 	github.com/itchyny/gojq v0.12.15
+	github.com/leaanthony/go-ansi-parser v1.6.1
 	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
 	github.com/muesli/reflow v0.3.0
-	github.com/muesli/termenv v0.15.2
+	github.com/muesli/termenv v0.16.0
 	github.com/stretchr/testify v1.7.0
 	github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e
-	golang.org/x/sys v0.28.0
-	golang.org/x/term v0.27.0
-	golang.org/x/text v0.21.0
+	golang.org/x/sys v0.31.0
+	golang.org/x/term v0.30.0
+	golang.org/x/text v0.23.0
 	gopkg.in/h2non/gock.v1 v1.1.2
 	gopkg.in/yaml.v3 v3.0.1
 )
@@ -30,14 +32,16 @@ require (
 	dario.cat/mergo v1.0.1 // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/semver/v3 v3.3.0 // indirect
-	github.com/alecthomas/chroma/v2 v2.8.0 // indirect
 	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
 	github.com/aymerick/douceur v0.2.0 // indirect
-	github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f // indirect
+	github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
+	github.com/charmbracelet/x/ansi v0.8.0 // indirect
+	github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
+	github.com/charmbracelet/x/term v0.2.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/dlclark/regexp2 v1.4.0 // indirect
+	github.com/dlclark/regexp2 v1.11.0 // indirect
 	github.com/google/uuid v1.6.0 // indirect
-	github.com/gorilla/css v1.0.0 // indirect
+	github.com/gorilla/css v1.0.1 // indirect
 	github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
 	github.com/huandu/xstrings v1.5.0 // indirect
 	github.com/itchyny/timefmt-go v0.1.5 // indirect
@@ -45,18 +49,18 @@ require (
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
-	github.com/mattn/go-runewidth v0.0.15 // indirect
-	github.com/microcosm-cc/bluemonday v1.0.26 // indirect
+	github.com/mattn/go-runewidth v0.0.16 // indirect
+	github.com/microcosm-cc/bluemonday v1.0.27 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
-	github.com/olekukonko/tablewriter v0.0.5 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/shopspring/decimal v1.4.0 // indirect
 	github.com/spf13/cast v1.7.0 // indirect
-	github.com/yuin/goldmark v1.5.4 // indirect
-	github.com/yuin/goldmark-emoji v1.0.2 // indirect
-	golang.org/x/crypto v0.31.0 // indirect
-	golang.org/x/net v0.33.0 // indirect
+	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+	github.com/yuin/goldmark v1.7.8 // indirect
+	github.com/yuin/goldmark-emoji v1.0.5 // indirect
+	golang.org/x/crypto v0.35.0 // indirect
+	golang.org/x/net v0.36.0 // indirect
 	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
 )
diff --git a/go.sum b/go.sum
index 3bd8dd3..c62fe31 100644
--- a/go.sum
+++ b/go.sum
@@ -12,22 +12,32 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe
 github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
 github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
 github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
-github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
-github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
-github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264=
-github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw=
-github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
-github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
+github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
+github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
+github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
+github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
+github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
+github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
-github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng=
-github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps=
-github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c h1:0FwZb0wTiyalb8QQlILWyIuh3nF5wok6j9D9oUQwfQY=
-github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c/go.mod h1:EPP2QJ0ectp3zo6gx9f8oJGq8keirqPJ3XpYEI8wrrs=
-github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f h1:1BXkZqDueTOBECyDoFGRi0xMYgjJ6vvoPIkWyKOwzTc=
-github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f/go.mod h1:yQqGHmheaQfkqiJWjklPHVAq1dKbk8uGbcoS/lcKCJ0=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
+github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3 h1:hx6E25SvI2WiZdt/gxINcYBnHD7PE2Vr9auqwg5B05g=
+github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3/go.mod h1:ihVqv4/YOY5Fweu1cxajuQrwJFh3zU4Ukb4mHVNjq3s=
+github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc h1:nFRtCfZu/zkltd2lsLUPlVNv3ej/Atod9hcdbRZtlys=
+github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
+github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
+github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
+github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
+github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
+github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
+github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
+github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
+github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
 github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
 github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
 github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=
@@ -39,8 +49,8 @@ github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
-github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
+github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
+github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
 github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@@ -49,8 +59,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
-github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
+github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
+github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
 github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
 github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
 github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs=
@@ -71,8 +81,12 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
+github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
+github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
 github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@@ -80,27 +94,24 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
-github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
-github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
-github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
-github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
+github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
+github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
 github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
 github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
 github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
-github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
-github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
 github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
 github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
-github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
-github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -119,22 +130,26 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8=
 github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
-github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
-github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
-github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
+github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
+github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
+github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
-golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
+golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
+golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
+golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
-golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
+golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -146,18 +161,18 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
-golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
+golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
diff --git a/internal/testutils/config_stub.go b/internal/testutils/config_stub.go
new file mode 100644
index 0000000..ad5eeb2
--- /dev/null
+++ b/internal/testutils/config_stub.go
@@ -0,0 +1,21 @@
+package testutils
+
+import (
+	"testing"
+
+	"github.com/cli/go-gh/v2/pkg/config"
+)
+
+// StubConfig replaces the config.Read function with a function that returns a config object
+// created from the given config string. It also sets up a cleanup function that restores the
+// original config.Read function.
+func StubConfig(t *testing.T, cfgStr string) {
+	t.Helper()
+	old := config.Read
+	config.Read = func(_ *config.Config) (*config.Config, error) {
+		return config.ReadFromString(cfgStr), nil
+	}
+	t.Cleanup(func() {
+		config.Read = old
+	})
+}
diff --git a/internal/yamlmap/yaml_map.go b/internal/yamlmap/yaml_map.go
index 78d0991..a440047 100644
--- a/internal/yamlmap/yaml_map.go
+++ b/internal/yamlmap/yaml_map.go
@@ -72,7 +72,7 @@ func (m *Map) AddEntry(key string, value *Map) {
 }
 
 func (m *Map) Empty() bool {
-	return m.Content == nil || len(m.Content) == 0
+	return len(m.Content) == 0
 }
 
 func (m *Map) FindEntry(key string) (*Map, error) {
diff --git a/pkg/api/client_options_test.go b/pkg/api/client_options_test.go
index 237b9d5..35cfb34 100644
--- a/pkg/api/client_options_test.go
+++ b/pkg/api/client_options_test.go
@@ -4,11 +4,12 @@ import (
 	"net/http"
 	"testing"
 
+	"github.com/cli/go-gh/v2/internal/testutils"
 	"github.com/stretchr/testify/assert"
 )
 
 func TestResolveOptions(t *testing.T) {
-	stubConfig(t, testConfigWithSocket())
+	testutils.StubConfig(t, testConfigWithSocket())
 
 	tests := []struct {
 		name          string
diff --git a/pkg/api/graphql_client_test.go b/pkg/api/graphql_client_test.go
index b27d38f..15cdc7a 100644
--- a/pkg/api/graphql_client_test.go
+++ b/pkg/api/graphql_client_test.go
@@ -7,12 +7,13 @@ import (
 	"testing"
 	"time"
 
+	"github.com/cli/go-gh/v2/internal/testutils"
 	"github.com/stretchr/testify/assert"
 	"gopkg.in/h2non/gock.v1"
 )
 
 func TestGraphQLClient(t *testing.T) {
-	stubConfig(t, testConfig())
+	testutils.StubConfig(t, testConfig())
 	t.Cleanup(gock.Off)
 
 	gock.New("https://api.github.com").
@@ -34,7 +35,7 @@ func TestGraphQLClient(t *testing.T) {
 }
 
 func TestGraphQLClientDoError(t *testing.T) {
-	stubConfig(t, testConfig())
+	testutils.StubConfig(t, testConfig())
 	t.Cleanup(gock.Off)
 
 	gock.New("https://api.github.com").
@@ -56,7 +57,7 @@ func TestGraphQLClientDoError(t *testing.T) {
 }
 
 func TestGraphQLClientQueryError(t *testing.T) {
-	stubConfig(t, testConfig())
+	testutils.StubConfig(t, testConfig())
 	t.Cleanup(gock.Off)
 
 	gock.New("https://api.github.com").
@@ -78,7 +79,7 @@ func TestGraphQLClientQueryError(t *testing.T) {
 }
 
 func TestGraphQLClientMutateError(t *testing.T) {
-	stubConfig(t, testConfig())
+	testutils.StubConfig(t, testConfig())
 	t.Cleanup(gock.Off)
 
 	gock.New("https://api.github.com").
diff --git a/pkg/api/http_client_test.go b/pkg/api/http_client_test.go
index 9fe0934..19c911d 100644
--- a/pkg/api/http_client_test.go
+++ b/pkg/api/http_client_test.go
@@ -8,13 +8,13 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/cli/go-gh/v2/pkg/config"
+	"github.com/cli/go-gh/v2/internal/testutils"
 	"github.com/stretchr/testify/assert"
 	"gopkg.in/h2non/gock.v1"
 )
 
 func TestHTTPClient(t *testing.T) {
-	stubConfig(t, testConfig())
+	testutils.StubConfig(t, testConfig())
 	t.Cleanup(gock.Off)
 
 	gock.New("https://api.github.com").
@@ -177,17 +177,6 @@ func defaultHeaders() http.Header {
 	return h
 }
 
-func stubConfig(t *testing.T, cfgStr string) {
-	t.Helper()
-	old := config.Read
-	config.Read = func(_ *config.Config) (*config.Config, error) {
-		return config.ReadFromString(cfgStr), nil
-	}
-	t.Cleanup(func() {
-		config.Read = old
-	})
-}
-
 func printPendingMocks(mocks []gock.Mock) string {
 	paths := []string{}
 	for _, mock := range mocks {
diff --git a/pkg/api/rest_client_test.go b/pkg/api/rest_client_test.go
index bc2917e..51ac16a 100644
--- a/pkg/api/rest_client_test.go
+++ b/pkg/api/rest_client_test.go
@@ -8,12 +8,13 @@ import (
 	"testing"
 	"time"
 
+	"github.com/cli/go-gh/v2/internal/testutils"
 	"github.com/stretchr/testify/assert"
 	"gopkg.in/h2non/gock.v1"
 )
 
 func TestRESTClient(t *testing.T) {
-	stubConfig(t, testConfig())
+	testutils.StubConfig(t, testConfig())
 	t.Cleanup(gock.Off)
 
 	gock.New("https://api.github.com").
diff --git a/pkg/markdown/markdown.go b/pkg/markdown/markdown.go
index ec05b5a..c7f589d 100644
--- a/pkg/markdown/markdown.go
+++ b/pkg/markdown/markdown.go
@@ -6,6 +6,8 @@ import (
 	"strings"
 
 	"github.com/charmbracelet/glamour"
+	xcolor "github.com/cli/go-gh/v2/pkg/x/color"
+	xmarkdown "github.com/cli/go-gh/v2/pkg/x/markdown"
 )
 
 // WithoutIndentation is a rendering option that removes indentation from the markdown rendering.
@@ -36,6 +38,12 @@ func WithTheme(theme string) glamour.TermRendererOption {
 	if style == "" || style == "auto" {
 		switch theme {
 		case "light", "dark":
+			if xcolor.IsAccessibleColorsEnabled() {
+				return glamour.WithOptions(
+					glamour.WithStyles(xmarkdown.AccessibleStyleConfig(theme)),
+					glamour.WithChromaFormatter("terminal16"),
+				)
+			}
 			style = theme
 		default:
 			style = "notty"
@@ -55,7 +63,7 @@ func Render(text string, opts ...glamour.TermRendererOption) (string, error) {
 	// Glamour rendering preserves carriage return characters in code blocks, but
 	// we need to ensure that no such characters are present in the output.
 	text = strings.ReplaceAll(text, "\r\n", "\n")
-	opts = append(opts, glamour.WithEmoji(), glamour.WithPreservedNewLines())
+	opts = append(opts, glamour.WithEmoji(), glamour.WithPreservedNewLines(), glamour.WithTableWrap(false))
 	tr, err := glamour.NewTermRenderer(opts...)
 	if err != nil {
 		return "", err
diff --git a/pkg/markdown/markdown_test.go b/pkg/markdown/markdown_test.go
index f131b42..8380ca7 100644
--- a/pkg/markdown/markdown_test.go
+++ b/pkg/markdown/markdown_test.go
@@ -1,38 +1,352 @@
 package markdown
 
 import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
 	"testing"
 
+	"github.com/MakeNowJust/heredoc"
+	"github.com/alecthomas/chroma/v2/styles"
+	"github.com/cli/go-gh/v2/pkg/x/color"
+	ansi "github.com/leaanthony/go-ansi-parser"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
-func Test_Render(t *testing.T) {
+// glamour theme colors found at https://github.com/charmbracelet/glamour/tree/master/styles
+const (
+	glamourLightH2_8bitColorSeq = "\x1b[38;5;27;"
+	glamourDarkH2_8bitColorSeq  = "\x1b[38;5;39;"
+	customH2_8bitColorSeq       = "\x1b[38;5;61;"
+	magenta_4bitColorSeq        = "\x1b[35;"
+	brightMagenta_4bitColorSeq  = "\x1b[95;"
+)
+
+// Test_RenderAccessible tests rendered markdown for accessibility concerns such as color mode / depth and other display attributes.
+// It works by parsing the rendered markdown for ANSI escape sequences and checking their display attributes.
+// Test scenarios allow multiple color mode / depths because `ansi.Parse()` considers `\x1b[0m` sequence as part of `ansi.Default`.
+func Test_RenderAccessible(t *testing.T) {
+	anchor := heredoc.Doc(`
+		[GitHub CLI repository](https://github.com/cli/cli)
+	`)
+
+	img := heredoc.Doc(`
+		[Animated Mona for loading screen](https://github.com/user-attachments/assets/a43e7ce6-8360-466c-b2d5-0c74a60c30a4)
+	`)
+
+	goCodeBlock := heredoc.Docf(`
+		%[1]s%[1]s%[1]sgo
+		package main
+
+		import (
+			"fmt"
+		)
+
+		func main() {
+			fmt.Println("Hello, world!")
+		}
+		%[1]s%[1]s%[1]s
+	`, "`")
+
+	shellCodeBlock := heredoc.Docf(`
+		%[1]s%[1]s%[1]sshell
+		# list all repositories for a user
+		$ gh api graphql --paginate -f query='
+			query($endCursor: String) {
+				viewer {
+					repositories(first: 100, after: $endCursor) {
+						nodes { nameWithOwner }
+						pageInfo {
+							hasNextPage
+							endCursor
+						}
+					}
+				}
+			}
+		'
+		%[1]s%[1]s%[1]s
+	`, "`")
+
+	tests := []struct {
+		name              string
+		text              string
+		theme             string
+		accessible        bool
+		wantColourModes   []ansi.ColourMode
+		allowDimFaintText bool
+	}{
+		// Go block
+		{
+			name:              "when the light theme is selected, the Go codeblock renders using 8-bit colors",
+			text:              goCodeBlock,
+			theme:             "light",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the dark theme is selected, the Go codeblock renders using 8-bit colors",
+			text:              goCodeBlock,
+			theme:             "dark",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the accessible env var is set and the light theme is selected, the Go codeblock renders using 4-bit colors without dim/faint text",
+			text:              goCodeBlock,
+			theme:             "light",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		{
+			name:              "when the accessible env var is set and the dark theme is selected, the Go codeblock renders using 4-bit colors without dim/faint text",
+			text:              goCodeBlock,
+			theme:             "dark",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		// shell block
+		{
+			name:              "when the light theme is selected, the Shell codeblock renders using 8-bit colors",
+			text:              shellCodeBlock,
+			theme:             "light",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the dark theme is selected, the Shell codeblock renders using 8-bit colors",
+			text:              shellCodeBlock,
+			theme:             "dark",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the accessible env var is set and the light theme is selected, the Shell codeblock renders using 4-bit colors without dim/faint text",
+			text:              shellCodeBlock,
+			theme:             "light",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		{
+			name:              "when the accessible env var is set and the dark theme is selected, the Shell codeblock renders using 4-bit colors without dim/faint text",
+			text:              shellCodeBlock,
+			theme:             "dark",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		// image text and link
+		{
+			name:              "when the light theme is selected, the image text and link rendered using 8-bit colors",
+			text:              img,
+			theme:             "light",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the dark theme is selected, the image text and link rendered using 8-bit colors",
+			text:              img,
+			theme:             "dark",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the accessible env var is set and the light theme is selected, the image text and link render using 4-bit colors without dim/faint text",
+			text:              img,
+			theme:             "light",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		{
+			name:              "when the accessible env var is set and the dark theme is selected, the image text and link render using 4-bit colors without dim/faint text",
+			text:              img,
+			theme:             "dark",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		// anchor text and link
+		{
+			name:              "when the light theme is selected, the anchor text and link rendered using 8-bit colors",
+			text:              anchor,
+			theme:             "light",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the dark theme is selected, the anchor text and link rendered using 8-bit colors",
+			text:              anchor,
+			theme:             "dark",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the accessible env var is set and the light theme is selected, the anchor text and link render using 4-bit colors without dim/faint text",
+			text:              anchor,
+			theme:             "light",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		{
+			name:              "when the accessible env var is set and the dark theme is selected, the anchor text and link render using 4-bit colors without dim/faint text",
+			text:              anchor,
+			theme:             "dark",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.accessible {
+				t.Setenv(color.AccessibleColorsEnv, "true")
+			}
+
+			out, err := Render(tt.text, WithTheme(tt.theme))
+			require.NoError(t, err)
+
+			// Parse and test ANSI escape sequences from rendered markdown for inaccessible display attributes
+			styledText, err := ansi.Parse(out)
+			require.NoError(t, err)
+
+			for _, st := range styledText {
+				require.Containsf(t, tt.wantColourModes, st.ColourMode, "Unexpected color mode detected in '%s' at %d", st, st.Offset)
+
+				if st.Faint() {
+					require.Truef(t, tt.allowDimFaintText, "Unexpected dim/faint text detected in '%s' at %d", st, st.Offset)
+				}
+			}
+		})
+	}
+}
+
+// Test_RenderColor verifies that the proper ANSI color codes are applied to the rendered
+// markdown by examining the ANSI escape sequences in the output for the correct color
+// match. For more information on ANSI color codes, see
+// https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
+func Test_RenderColor(t *testing.T) {
 	t.Setenv("GLAMOUR_STYLE", "")
+
+	codeBlock := heredoc.Docf(`
+		%[1]s%[1]s%[1]sgo
+		fmt.Println("Hello, world!")
+		%[1]s%[1]s%[1]s
+	`, "`")
+
 	tests := []struct {
-		name  string
-		text  string
-		theme string
+		name             string
+		text             string
+		theme            string
+		styleEnvVar      string
+		accessibleEnvVar string
+		wantOut          string
 	}{
 		{
-			name:  "light style",
-			text:  "some text",
-			theme: "light",
+			name:    "when the light theme is selected, the h2 renders using the 8-bit blue 27 provided by glamour",
+			text:    "## h2",
+			theme:   "light",
+			wantOut: fmt.Sprintf("%s1mh2", glamourLightH2_8bitColorSeq),
 		},
 		{
-			name:  "dark style",
-			text:  "some text",
-			theme: "dark",
+			name:    "when the dark theme is selected, the h2 renders using the 8-bit blue 39 provided by glamour",
+			text:    "## h2",
+			theme:   "dark",
+			wantOut: fmt.Sprintf("%s1mh2", glamourDarkH2_8bitColorSeq),
 		},
 		{
-			name:  "notty style",
-			text:  "some text",
-			theme: "none",
+			name:    "when no theme is selected, the h2 renders in plain text without ansi coloring",
+			text:    "## h2",
+			theme:   "none",
+			wantOut: "## h2",
+		},
+		{
+			name:        "when the style env var is set, we override the theme with that style",
+			text:        "## h2",
+			theme:       "light",
+			styleEnvVar: "customStyle",
+			wantOut:     fmt.Sprintf("%s1mh2", customH2_8bitColorSeq),
+		},
+		{
+			name:             "when the accessible env var is set and the light theme is selected, the h2 renders using the 4-bit magenta provided by the light accessible style",
+			text:             "## h2",
+			theme:            "light",
+			accessibleEnvVar: "true",
+			wantOut:          fmt.Sprintf("%s1mh2", magenta_4bitColorSeq),
+		},
+		{
+			name:             "when the accessible env var is set and the dark theme is selected, the h2 renders using the 4-bit bright magenta provided by the dark accessible style",
+			text:             "## h2",
+			theme:            "dark",
+			accessibleEnvVar: "true",
+			wantOut:          fmt.Sprintf("%s1mh2", brightMagenta_4bitColorSeq),
+		},
+		{
+			name:    "when the light theme is selected, the codeblock renders using 8-bit colors",
+			text:    codeBlock,
+			theme:   "light",
+			wantOut: "\x1b[0m\x1b[38;5;235mfmt\x1b[0m\x1b[38;5;210m.\x1b[0m\x1b[38;5;35mPrintln\x1b[0m\x1b[38;5;210m(\x1b[0m\x1b[38;5;95m\"Hello, world!\"\x1b[0m\x1b[38;5;210m)\x1b[0m",
+		},
+		{
+			name:    "when the dark theme is selected, the codeblock renders using 8-bit colors",
+			text:    codeBlock,
+			theme:   "dark",
+			wantOut: "\x1b[0m\x1b[38;5;251mfmt\x1b[0m\x1b[38;5;187m.\x1b[0m\x1b[38;5;42mPrintln\x1b[0m\x1b[38;5;187m(\x1b[0m\x1b[38;5;173m\"Hello, world!\"\x1b[0m\x1b[38;5;187m)\x1b[0m",
+		},
+		{
+			name:             "when the accessible env var is set and the light theme is selected, the codeblock renders using 4-bit colors",
+			text:             codeBlock,
+			theme:            "light",
+			accessibleEnvVar: "true",
+			wantOut:          "\x1b[0m\x1b[30mfmt\x1b[0m\x1b[33m.\x1b[0m\x1b[36mPrintln\x1b[0m\x1b[33m(\x1b[0m\x1b[90m\"Hello, world!\"\x1b[0m\x1b[33m)\x1b[0m",
+		},
+		{
+			name:             "when the accessible env var is set and the dark theme is selected, the codeblock renders using 4-bit colors",
+			text:             codeBlock,
+			theme:            "dark",
+			accessibleEnvVar: "true",
+			wantOut:          "\x1b[0m\x1b[37mfmt\x1b[0m\x1b[37m.\x1b[0m\x1b[36mPrintln\x1b[0m\x1b[37m(\x1b[0m\x1b[33m\"Hello, world!\"\x1b[0m\x1b[37m)\x1b[0m",
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			_, err := Render(tt.text, WithTheme(tt.theme))
-			assert.NoError(t, err)
+			t.Cleanup(func() {
+				// Chroma caches charm style used to render codeblocks, it must be unregistered to avoid previously used style being reused.
+				delete(styles.Registry, "charm")
+			})
+			t.Setenv("GH_ACCESSIBLE_COLORS", tt.accessibleEnvVar)
+
+			if tt.styleEnvVar != "" {
+				path := filepath.Join(t.TempDir(), fmt.Sprintf("%s.json", tt.styleEnvVar))
+				err := os.WriteFile(path, []byte(customGlamourStyle(t)), 0644)
+				require.NoError(t, err)
+				t.Setenv("GLAMOUR_STYLE", path)
+			}
+
+			out, err := Render(tt.text, WithTheme(tt.theme))
+			require.NoError(t, err)
+			assert.Contains(t, out, tt.wantOut)
 		})
 	}
 }
+
+func customGlamourStyle(t *testing.T) string {
+	t.Helper()
+	colorCode := strings.Split(customH2_8bitColorSeq, ";")[2]
+	return fmt.Sprintf(`
+{
+	"heading": {
+		"block_suffix": "\n",
+		"color": "%s",
+		"bold": true
+	},
+	"h2": {
+		"prefix": "## "
+	}
+}`, colorCode)
+}
diff --git a/pkg/repository/repository_test.go b/pkg/repository/repository_test.go
index dfcdca7..0430eab 100644
--- a/pkg/repository/repository_test.go
+++ b/pkg/repository/repository_test.go
@@ -3,12 +3,12 @@ package repository
 import (
 	"testing"
 
-	"github.com/cli/go-gh/v2/pkg/config"
+	"github.com/cli/go-gh/v2/internal/testutils"
 	"github.com/stretchr/testify/assert"
 )
 
 func TestParse(t *testing.T) {
-	stubConfig(t, "")
+	testutils.StubConfig(t, "")
 
 	tests := []struct {
 		name         string
@@ -106,7 +106,7 @@ hosts:
     oauth_token: yyyyyyyyyyyyyyyyyyyy
     git_protocol: https
 `
-	stubConfig(t, cfgStr)
+	testutils.StubConfig(t, cfgStr)
 	r, err := Parse("OWNER/REPO")
 	assert.NoError(t, err)
 	assert.Equal(t, "enterprise.com", r.Host)
@@ -189,14 +189,3 @@ func TestParseWithHost(t *testing.T) {
 		})
 	}
 }
-
-func stubConfig(t *testing.T, cfgStr string) {
-	t.Helper()
-	old := config.Read
-	config.Read = func(_ *config.Config) (*config.Config, error) {
-		return config.ReadFromString(cfgStr), nil
-	}
-	t.Cleanup(func() {
-		config.Read = old
-	})
-}
diff --git a/pkg/x/color/accessibility.go b/pkg/x/color/accessibility.go
new file mode 100644
index 0000000..e467140
--- /dev/null
+++ b/pkg/x/color/accessibility.go
@@ -0,0 +1,39 @@
+package color
+
+import (
+	"os"
+
+	"github.com/cli/go-gh/v2/pkg/config"
+)
+
+const (
+	// AccessibleColorsEnv is the name of the environment variable to enable accessibile color features.
+	AccessibleColorsEnv = "GH_ACCESSIBLE_COLORS"
+
+	// AccessibleColorsSetting is the name of the `gh config` setting to enable accessibile color features.
+	AccessibleColorsSetting = "accessible_colors"
+)
+
+// IsAccessibleColorsEnabled returns true if accessible colors are enabled via environment variable
+// or configuration setting with the environment variable having higher precedence.
+//
+// If the environment variable is set, then any value other than the following will enable accessible colors:
+// empty, "0", "false", or "no".
+func IsAccessibleColorsEnabled() bool {
+	// Environment variable only has the highest precedence if actually set.
+	if envVar, set := os.LookupEnv(AccessibleColorsEnv); set {
+		switch envVar {
+		case "", "0", "false", "no":
+			return false
+		default:
+			return true
+		}
+	}
+
+	// We are not handling errors because we don't want to fail if the config is not
+	// read. Instead, we assume an empty configuration is equivalent to "disabled".
+	cfg, _ := config.Read(nil)
+	accessibleConfigValue, _ := cfg.Get([]string{AccessibleColorsSetting})
+
+	return accessibleConfigValue == "enabled"
+}
diff --git a/pkg/x/color/accessibility_test.go b/pkg/x/color/accessibility_test.go
new file mode 100644
index 0000000..6f3bae2
--- /dev/null
+++ b/pkg/x/color/accessibility_test.go
@@ -0,0 +1,119 @@
+package color
+
+import (
+	"testing"
+
+	"github.com/MakeNowJust/heredoc"
+	"github.com/cli/go-gh/v2/internal/testutils"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIsAccessibleColorsEnabled(t *testing.T) {
+	tests := []struct {
+		name    string
+		env     map[string]string
+		cfgStr  string
+		wantOut bool
+	}{
+		{
+			name:    "When the accessibility configuration and env var are both unset, it should return false",
+			cfgStr:  "",
+			wantOut: false,
+		},
+		{
+			name: "When the accessibility configuration is unset but the env var is set to something truthy (not '0' or 'false'), it should return true",
+			env: map[string]string{
+				"GH_ACCESSIBLE_COLORS": "1",
+			},
+			cfgStr:  "",
+			wantOut: true,
+		},
+		{
+			name: "When the accessibility configuration is unset and the env var returns '0', it should return false",
+			env: map[string]string{
+				"GH_ACCESSIBLE_COLORS": "0",
+			},
+			cfgStr:  "",
+			wantOut: false,
+		},
+		{
+			name: "When the accessibility configuration is unset and the env var returns 'false', it should return false",
+			env: map[string]string{
+				"GH_ACCESSIBLE_COLORS": "false",
+			},
+			cfgStr:  "",
+			wantOut: false,
+		},
+		{
+			name: "When the accessibility configuration is unset and the env var returns '', it should return false",
+			env: map[string]string{
+				"GH_ACCESSIBLE_COLORS": "",
+			},
+			cfgStr:  "",
+			wantOut: false,
+		},
+		{
+			name:    "When the accessibility configuration is set to enabled and the env var is unset, it should return true",
+			cfgStr:  accessibilityEnabledConfig(),
+			wantOut: true,
+		},
+		{
+			name:    "When the accessibility configuration is set to disabled and the env var is unset, it should return false",
+			cfgStr:  accessibilityDisabledConfig(),
+			wantOut: false,
+		},
+		{
+			name: "When the accessibility configuration is set to disabled and the env var is set to something truthy (not '0' or 'false'), it should return true",
+			env: map[string]string{
+				"GH_ACCESSIBLE_COLORS": "true",
+			},
+			cfgStr:  accessibilityDisabledConfig(),
+			wantOut: true,
+		},
+		{
+			name: "When the accessibility configuration is set to enabled and the env var is set to '0', it should return false",
+			env: map[string]string{
+				"GH_ACCESSIBLE_COLORS": "0",
+			},
+			cfgStr:  accessibilityEnabledConfig(),
+			wantOut: false,
+		},
+		{
+			name: "When the accessibility configuration is set to enabled and the env var is set to 'false', it should return false",
+			env: map[string]string{
+				"GH_ACCESSIBLE_COLORS": "false",
+			},
+			cfgStr:  accessibilityEnabledConfig(),
+			wantOut: false,
+		},
+		{
+			name: "When the accessibility configuration is set to enabled and the env var is set to '', it should return false",
+			env: map[string]string{
+				"GH_ACCESSIBLE_COLORS": "",
+			},
+			cfgStr:  accessibilityEnabledConfig(),
+			wantOut: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			for k, v := range tt.env {
+				t.Setenv(k, v)
+			}
+			testutils.StubConfig(t, tt.cfgStr)
+			assert.Equal(t, tt.wantOut, IsAccessibleColorsEnabled())
+		})
+	}
+}
+
+func accessibilityEnabledConfig() string {
+	return heredoc.Doc(`
+		accessible_colors: enabled
+	`)
+}
+
+func accessibilityDisabledConfig() string {
+	return heredoc.Doc(`
+		accessible_colors: disabled
+	`)
+}
diff --git a/pkg/x/color/color.go b/pkg/x/color/color.go
new file mode 100644
index 0000000..ab18c45
--- /dev/null
+++ b/pkg/x/color/color.go
@@ -0,0 +1,4 @@
+// Package color handles experimental GitHub CLI user experiences focused on color rendering concerns such as accessibility and color roles.
+//
+// Note this is an experimental package where the API is subject to change.
+package color
diff --git a/pkg/x/markdown/accessibility.go b/pkg/x/markdown/accessibility.go
new file mode 100644
index 0000000..de48c51
--- /dev/null
+++ b/pkg/x/markdown/accessibility.go
@@ -0,0 +1,155 @@
+package markdown
+
+import (
+	"fmt"
+	"strconv"
+
+	"github.com/charmbracelet/glamour/ansi"
+	"github.com/charmbracelet/glamour/styles"
+)
+
+// glamourStyleColor represents color codes used to customize glamour style elements.
+type glamourStyleColor int
+
+// Do not change the order of the following glamour color constants,
+// which matches 4-bit colors with their respective color codes.
+const (
+	black glamourStyleColor = iota
+	red
+	green
+	yellow
+	blue
+	magenta
+	cyan
+	white
+	brightBlack
+	brightRed
+	brightGreen
+	brightYellow
+	brightBlue
+	brightMagenta
+	brightCyan
+	brightWhite
+)
+
+func (gsc glamourStyleColor) code() *string {
+	s := strconv.Itoa(int(gsc))
+	return &s
+}
+
+func parseGlamourStyleColor(code string) (glamourStyleColor, error) {
+	switch code {
+	case "0":
+		return black, nil
+	case "1":
+		return red, nil
+	case "2":
+		return green, nil
+	case "3":
+		return yellow, nil
+	case "4":
+		return blue, nil
+	case "5":
+		return magenta, nil
+	case "6":
+		return cyan, nil
+	case "7":
+		return white, nil
+	case "8":
+		return brightBlack, nil
+	case "9":
+		return brightRed, nil
+	case "10":
+		return brightGreen, nil
+	case "11":
+		return brightYellow, nil
+	case "12":
+		return brightBlue, nil
+	case "13":
+		return brightMagenta, nil
+	case "14":
+		return brightCyan, nil
+	case "15":
+		return brightWhite, nil
+	default:
+		return 0, fmt.Errorf("invalid color code: %s", code)
+	}
+}
+
+func AccessibleStyleConfig(theme string) ansi.StyleConfig {
+	switch theme {
+	case "light":
+		return accessibleLightStyleConfig()
+	case "dark":
+		return accessibleDarkStyleConfig()
+	default:
+		return ansi.StyleConfig{}
+	}
+}
+
+func accessibleDarkStyleConfig() ansi.StyleConfig {
+	cfg := styles.DarkStyleConfig
+
+	// Text color
+	cfg.Document.StylePrimitive.Color = white.code()
+
+	// Link colors
+	cfg.Link.Color = brightCyan.code()
+	cfg.LinkText.Color = brightCyan.code()
+
+	// Heading colors
+	cfg.Heading.StylePrimitive.Color = brightMagenta.code()
+	cfg.H1.StylePrimitive.Color = brightWhite.code()
+	cfg.H1.StylePrimitive.BackgroundColor = brightBlue.code()
+	cfg.H6.StylePrimitive.Color = brightMagenta.code()
+
+	// Code colors
+	cfg.Code.BackgroundColor = brightWhite.code()
+	cfg.Code.Color = red.code()
+
+	// Image colors
+	cfg.Image.Color = brightMagenta.code()
+	cfg.ImageText.Color = brightMagenta.code()
+
+	// Horizontal rule colors
+	cfg.HorizontalRule.Color = white.code()
+
+	// Code block colors
+	// Unsetting StyleBlock color until we understand what it does versus Chroma style
+	cfg.CodeBlock.StyleBlock.StylePrimitive.Color = nil
+
+	return cfg
+}
+
+func accessibleLightStyleConfig() ansi.StyleConfig {
+	cfg := styles.LightStyleConfig
+
+	// Text color
+	cfg.Document.StylePrimitive.Color = black.code()
+
+	// Link colors
+	cfg.Link.Color = brightBlue.code()
+	cfg.LinkText.Color = brightBlue.code()
+
+	// Heading colors
+	cfg.Heading.StylePrimitive.Color = magenta.code()
+	cfg.H1.StylePrimitive.Color = brightWhite.code()
+	cfg.H1.StylePrimitive.BackgroundColor = blue.code()
+
+	// Code colors
+	cfg.Code.BackgroundColor = brightWhite.code()
+	cfg.Code.Color = red.code()
+
+	// Image colors
+	cfg.Image.Color = magenta.code()
+	cfg.ImageText.Color = magenta.code()
+
+	// Horizontal rule colors
+	cfg.HorizontalRule.Color = white.code()
+
+	// Code block colors
+	// Unsetting StyleBlock color until we understand what it does versus Chroma style
+	cfg.CodeBlock.StyleBlock.StylePrimitive.Color = nil
+
+	return cfg
+}
diff --git a/pkg/x/markdown/accessibility_test.go b/pkg/x/markdown/accessibility_test.go
new file mode 100644
index 0000000..5e0ccc4
--- /dev/null
+++ b/pkg/x/markdown/accessibility_test.go
@@ -0,0 +1,234 @@
+package markdown
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/charmbracelet/glamour/ansi"
+	"github.com/charmbracelet/glamour/styles"
+	"github.com/stretchr/testify/assert"
+)
+
+// TestGlamourStyleColors ensures that the resulting string color codes match the expected values.
+func TestGlamourStyleColors(t *testing.T) {
+	tests := []struct {
+		name string
+		c    glamourStyleColor
+		want string
+	}{
+		{
+			name: "black",
+			c:    black,
+			want: "0",
+		},
+		{
+			name: "red",
+			c:    red,
+			want: "1",
+		},
+		{
+			name: "green",
+			c:    green,
+			want: "2",
+		},
+		{
+			name: "yellow",
+			c:    yellow,
+			want: "3",
+		},
+		{
+			name: "blue",
+			c:    blue,
+			want: "4",
+		},
+		{
+			name: "magenta",
+			c:    magenta,
+			want: "5",
+		},
+		{
+			name: "cyan",
+			c:    cyan,
+			want: "6",
+		},
+		{
+			name: "white",
+			c:    white,
+			want: "7",
+		},
+		{
+			name: "bright black",
+			c:    brightBlack,
+			want: "8",
+		},
+		{
+			name: "bright red",
+			c:    brightRed,
+			want: "9",
+		},
+		{
+			name: "bright green",
+			c:    brightGreen,
+			want: "10",
+		},
+		{
+			name: "bright yellow",
+			c:    brightYellow,
+			want: "11",
+		},
+		{
+			name: "bright blue",
+			c:    brightBlue,
+			want: "12",
+		},
+		{
+			name: "bright magenta",
+			c:    brightMagenta,
+			want: "13",
+		},
+		{
+			name: "bright cyan",
+			c:    brightCyan,
+			want: "14",
+		},
+		{
+			name: "bright white",
+			c:    brightWhite,
+			want: "15",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			assert.Equal(t, tt.want, *tt.c.code())
+		})
+	}
+}
+
+func TestAccessibleStyleConfig(t *testing.T) {
+	tests := []struct {
+		name  string
+		theme string
+		want  ansi.StyleConfig
+	}{
+		{
+			name:  "light",
+			theme: "light",
+			want:  accessibleLightStyleConfig(),
+		},
+		{
+			name:  "dark",
+			theme: "dark",
+			want:  accessibleDarkStyleConfig(),
+		},
+		{
+			name:  "fallback",
+			theme: "foo",
+			want:  ansi.StyleConfig{},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			assert.Equal(t, tt.want, AccessibleStyleConfig(tt.theme))
+		})
+	}
+}
+
+func TestAccessibleDarkStyleConfig(t *testing.T) {
+	cfg := accessibleDarkStyleConfig()
+	assert.Equal(t, white.code(), cfg.Document.StylePrimitive.Color)
+	assert.Equal(t, brightCyan.code(), cfg.Link.Color)
+	assert.Equal(t, brightMagenta.code(), cfg.Heading.StylePrimitive.Color)
+	assert.Equal(t, brightWhite.code(), cfg.H1.StylePrimitive.Color)
+	assert.Equal(t, brightBlue.code(), cfg.H1.StylePrimitive.BackgroundColor)
+	assert.Equal(t, brightWhite.code(), cfg.Code.BackgroundColor)
+	assert.Equal(t, red.code(), cfg.Code.Color)
+	assert.Equal(t, brightMagenta.code(), cfg.Image.Color)
+	assert.Equal(t, white.code(), cfg.HorizontalRule.Color)
+
+	// Test that we haven't changed the original style
+	assert.Equal(t, styles.DarkStyleConfig.H2, cfg.H2)
+}
+
+func TestAccessibleDarkStyleConfigIs4Bit(t *testing.T) {
+	t.Parallel()
+
+	cfg := accessibleDarkStyleConfig()
+	validateColors(t, reflect.ValueOf(cfg), "StyleConfig")
+}
+
+func TestAccessibleLightStyleConfig(t *testing.T) {
+	t.Parallel()
+
+	cfg := accessibleLightStyleConfig()
+	assert.Equal(t, black.code(), cfg.Document.StylePrimitive.Color)
+	assert.Equal(t, brightBlue.code(), cfg.Link.Color)
+	assert.Equal(t, magenta.code(), cfg.Heading.StylePrimitive.Color)
+	assert.Equal(t, brightWhite.code(), cfg.H1.StylePrimitive.Color)
+	assert.Equal(t, blue.code(), cfg.H1.StylePrimitive.BackgroundColor)
+	assert.Equal(t, brightWhite.code(), cfg.Code.BackgroundColor)
+	assert.Equal(t, red.code(), cfg.Code.Color)
+	assert.Equal(t, magenta.code(), cfg.Image.Color)
+	assert.Equal(t, white.code(), cfg.HorizontalRule.Color)
+
+	// Test that we haven't changed the original style
+	assert.Equal(t, styles.LightStyleConfig.H2, cfg.H2)
+}
+
+func TestAccessibleLightStyleConfigIs4Bit(t *testing.T) {
+	t.Parallel()
+
+	cfg := accessibleLightStyleConfig()
+	validateColors(t, reflect.ValueOf(cfg), "StyleConfig")
+}
+
+// Walk every field in the StyleConfig struct, checking that the Color and
+// BackgroundColor fields are valid 4-bit colors.
+//
+// This test skips Chroma fields because their Color fields are RGB hex values
+// that are downsampled to 4-bit colors unlike Glamour, which are 8-bit colors.
+// For more information, https://github.com/alecthomas/chroma/blob/0bf0e9f9ae2a81d463afe769cce01ff821bee3ba/formatters/tty_indexed.go#L32-L44
+func validateColors(t *testing.T, v reflect.Value, path string) {
+	if v.Kind() == reflect.Ptr {
+		if v.IsNil() {
+			return
+		}
+		v = v.Elem()
+	}
+
+	switch v.Kind() {
+	case reflect.Struct:
+		for i := range v.NumField() {
+			field := v.Field(i)
+			fieldType := v.Type().Field(i)
+
+			// Construct path for better error reporting
+			fieldPath := path + "." + fieldType.Name
+
+			// Ensure we only check Glamour "Color" and "BackgroundColor"
+			if fieldType.Name == "Chroma" {
+				continue
+			} else if (fieldType.Name == "Color" || fieldType.Name == "BackgroundColor") &&
+				fieldType.Type.Kind() == reflect.Ptr && fieldType.Type.Elem().Kind() == reflect.String {
+
+				if field.IsNil() {
+					continue
+				}
+				color := field.Elem().String()
+				_, err := parseGlamourStyleColor(color)
+				assert.NoError(t, err, "Failed to parse color '%s' in %s", color, fieldPath)
+			} else {
+				// Recurse into nested structs
+				validateColors(t, field, fieldPath)
+			}
+		}
+	case reflect.Slice:
+		// Handle slices of structs
+		for i := range v.Len() {
+			validateColors(t, v.Index(i), path+"[]")
+		}
+	}
+}
diff --git a/pkg/x/markdown/markdown.go b/pkg/x/markdown/markdown.go
new file mode 100644
index 0000000..268bda5
--- /dev/null
+++ b/pkg/x/markdown/markdown.go
@@ -0,0 +1,4 @@
+// Package markdown handles experimental GitHub CLI user experiences focused on markdown rendering concerns such as accessibility.
+//
+// Note this is an experimental package where the API is subject to change.
+package markdown
diff --git a/pkg/x/x.go b/pkg/x/x.go
new file mode 100644
index 0000000..c16f9d8
--- /dev/null
+++ b/pkg/x/x.go
@@ -0,0 +1,4 @@
+// Package x is a collection of experimental features for use within the GitHub CLI and CLI extensions.
+//
+// Anything contained is subject to change without notice until it is considered stable enough to be promoted.
+package x


Reviews:
user: login: williammartin
body: @andyfeller I got a bit distracted with some other stuff so I didn't get into the good stuff yet in this PR review sorry. Mostly just cursory stuff!
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T16:59:17Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T12:22:15Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T12:25:15Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T12:27:27Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T12:44:07Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T12:45:40Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T12:51:54Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T12:52:21Z
user: login: williammartin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T13:35:24Z
user: login: williammartin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T13:37:05Z
user: login: williammartin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T13:38:09Z
user: login: williammartin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T13:38:24Z
user: login: williammartin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-11T13:39:23Z
user: login: williammartin
body: Lots of good stuff in here, don't think there's too much work to resolve my comments.

I haven't yet pulled this into `cli/cli` to use it.

state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-11T16:05:44Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T16:25:13Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T20:38:49Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T20:39:00Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T20:42:55Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T20:46:22Z
user: login: williammartin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T10:10:00Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T12:24:46Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T12:26:10Z
user: login: williammartin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T13:07:41Z
user: login: williammartin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T13:09:52Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T14:04:24Z
user: login: williammartin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T14:05:51Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T22:33:52Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T22:36:36Z
user: login: williammartin
body: ### `./bin/gh repo view` with and without `GH_ACCESSIBLE_COLORS=1`

<img width="1920" alt="image showing markdown rendering" src="https://github.com/user-attachments/assets/e59db178-85fd-4e61-87d9-0e495b99d488" />

And on a white background with `GH_ACCESSIBLE_COLORS=1`

<img width="877" alt="image" src="https://github.com/user-attachments/assets/202b34a8-6aca-4d14-b34c-908ef81646f3" />

### With config setting

```
➜  cli git:(trunk) ✗ gh config set accessible_colors enabled
! warning: 'accessible_colors' is not a known configuration key
```

<img width="944" alt="image" src="https://github.com/user-attachments/assets/1661ca33-88c3-4881-a97b-797091e78395" />
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-13T10:53:25Z
user: login: williammartin
body: Requesting changes based on ^
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-13T11:16:00Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T21:37:14Z
user: login: williammartin
body: Approved with one non blocking comment! Well done on all this, I know it was a lot.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-25T10:32:19Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T11:48:29Z
user: login: andyfeller
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T11:54:41Z

Review comments:
diff_hunk: @@ -7,20 +7,21 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - name: Set up Go
-        uses: actions/setup-go@v3
-        with:
-          go-version: "1.21"
-
       - name: Checkout repository
         uses: actions/checkout@v4
 
+      - name: Set up Go
+        uses: actions/setup-go@v5
+        with:
+          go-version-file: go.mod
+
       - name: Check dependencies
         run: |
           go mod tidy
           git diff --exit-code go.mod
 
       - name: Lint
-        uses: golangci/golangci-lint-action@v3.4.0
+        uses: golangci/golangci-lint-action@v6
         with:
-          version: v1.54
+          version: v1.64
+          problem-matchers: true
path: .github/workflows/lint.yml
user: login: williammartin
body: **Question**

I read the [doc](https://github.com/golangci/golangci-lint-action?tab=readme-ov-file#problem-matchers) but I don't understand what this is doing. Help?
created_at: 2025-03-10T15:41:49Z
author_association: MEMBER
diff_hunk: @@ -7,20 +7,21 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - name: Set up Go
-        uses: actions/setup-go@v3
-        with:
-          go-version: "1.21"
-
       - name: Checkout repository
         uses: actions/checkout@v4
 
+      - name: Set up Go
path: .github/workflows/lint.yml
user: login: williammartin
body: **Question**

Why did you reorder the checkout and setup? Same for `lint.yml`
created_at: 2025-03-10T15:42:30Z
author_association: MEMBER
diff_hunk: @@ -72,7 +72,7 @@ func (m *Map) AddEntry(key string, value *Map) {
 }
 
 func (m *Map) Empty() bool {
-	return m.Content == nil || len(m.Content) == 0
+	return len(m.Content) == 0
path: internal/yamlmap/yaml_map.go
user: login: williammartin
body: **Question**

Can you explain this change please? I'm guessing it was linted because the `nil` slice has `len == 0` but just want to check!
created_at: 2025-03-10T15:44:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,21 @@
+package testutils
+
+import (
+	"testing"
+
+	"github.com/cli/go-gh/v2/pkg/config"
+)
+
+// StubConfig replaces the config.Read function with a function that returns a config object
+// created from the given config string. It also sets up a cleanup function that restores the
+// original config.Read function.
+func StubConfig(t *testing.T, cfgStr string) {
path: internal/testutils/config_stub.go
user: login: williammartin
body: **Food For Thought**

No concerns merging this as is because it's easy to change later, but maybe this should be `config.Stub` where this lives in `package config_test` would be better than a `testutils` package.
created_at: 2025-03-10T15:46:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,106 @@
+package markdown
+
+import (
+	"strconv"
+
+	"github.com/charmbracelet/glamour/ansi"
+	"github.com/charmbracelet/glamour/styles"
+)
+
+type ANSIColorCode int
+
+const (
+	Black ANSIColorCode = iota
+	Red
+	Green
+	Yellow
+	Blue
+	Magenta
+	Cyan
+	White
+	BrightBlack
+	BrightRed
+	BrightGreen
+	BrightYellow
+	BrightBlue
+	BrightMagenta
+	BrightCyan
+	BrightWhite
+)
+
+func (a ANSIColorCode) String() string {
+	return strconv.Itoa(int(a))
+}
+
+func (a ANSIColorCode) StrPtr() *string {
path: pkg/markdown/accessibility.go
user: login: williammartin
body: **Question**

Did you take this pattern from somewhere?
created_at: 2025-03-10T15:48:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,106 @@
+package markdown
path: pkg/markdown/accessibility.go
user: login: williammartin
body: **Question**

Should this also live under `x` for the moment?
created_at: 2025-03-10T15:50:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,40 @@
+package color
+
+import (
+	"os"
+
+	"github.com/cli/go-gh/v2/pkg/config"
+)
+
+const (
+	// AccessibleColorsEnv is the name of the environment variable to enable accessibile color features.
+	AccessibleColorsEnv = "GH_ACCESSIBLE_COLORS"
+
+	// AccessibleColorsSetting is the name of the `gh config` setting to enable accessibile color features.
+	AccessibleColorsSetting = "accessible_colors"
+)
+
+// IsAccessibleColorsEnabled returns true if accessible colors are enabled via environment variable
+// or configuration setting with the environment variable having higher precedence.
+//
+// If the environment variable is empty, "0", or "false", the accessible colors are disabled.
+// Any other value enables the accessibility feature.
+//
+// Note this is an experimental feature that is subject to change.
+func IsAccessibleColorsEnabled() bool {
+	envVar := os.Getenv(AccessibleColorsEnv)
+	if envVar != "" {
+		return isEnvVarEnabled(envVar)
+	}
+
+	// We are not handling errors because we don't want to fail if the config is not
+	// read. Instead, we assume an empty configuration is equivalent to "disabled".
+	cfg, _ := config.Read(nil)
+	accessibleConfigValue, _ := cfg.Get([]string{AccessibleColorsSetting})
+
+	return accessibleConfigValue == "enabled"
+}
+
+func isEnvVarEnabled(envVar string) bool {
+	return envVar != "" && envVar != "0" && envVar != "false"
path: pkg/x/color/accessibility.go
user: login: williammartin
body: **Food For Thought**

No specific request but this function looks a little sus in that the only place it is used is in this file and repeats the string check that wraps it on line 26.

I think I'd probably inline on 26 like if you wanted to keep the clarity:

```go
if envVar != "" {
   isEnVarEnabled := envVar != "0" && envVar != "false"
   return isEnvVarEnabled
}
```
created_at: 2025-03-10T15:55:09Z
author_association: MEMBER
diff_hunk: @@ -7,20 +7,21 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - name: Set up Go
-        uses: actions/setup-go@v3
-        with:
-          go-version: "1.21"
-
       - name: Checkout repository
         uses: actions/checkout@v4
 
+      - name: Set up Go
+        uses: actions/setup-go@v5
+        with:
+          go-version-file: go.mod
+
       - name: Check dependencies
         run: |
           go mod tidy
           git diff --exit-code go.mod
 
       - name: Lint
-        uses: golangci/golangci-lint-action@v3.4.0
+        uses: golangci/golangci-lint-action@v6
         with:
-          version: v1.54
+          version: v1.64
+          problem-matchers: true
path: .github/workflows/lint.yml
user: login: andyfeller
body: ### Background

One of the most esoteric features of GitHub Actions is the ability to use annotations, which CI can draw attention to problems or other notable aspects during workflow run.  Using one of the following [Workflow commands](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions), you are describing where GitHub Actions should display the annotation in the GitHub UI, which pops up as a little box to draw the viewers attention:

- [Setting a notice message](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-a-notice-message)

  ```
  ::notice file={name},line={line},endLine={endLine},title={title}::{message}
  ```

- [Setting a warning message](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-a-warning-message)

  ```
  ::notice file={name},line={line},endLine={endLine},title={title}::{message}
  ```
  
- [Setting an error message](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-an-error-message)

  ```
  ::error file={name},line={line},endLine={endLine},title={title}::{message}
  ```

However, this is a very specific format which people might not like or you might use tools that you don't control but want to bubble up certain problems.  This is where a [**problem matcher**](https://github.com/actions/runner/blob/0ce29d09c64461774c26cd23bc4610d3e94b1edd/docs/adrs/0276-problem-matchers.md#L4) is necessary to identify and translate output from GitHub Actions workflow run into annotations.  I've only found docs on this in the `actions/runner` ADRs; nothing in GitHub Docs.

### `actions/setup-go` problem matcher and `problem-matchers: true`

Now back to this change, `actions/setup-go` sets up a custom problem matcher, however I'm unsure the full story behind the format due to the lack of docs and commentary in https://github.com/actions/setup-go/pull/2:

- [`src/main.ts`](https://github.com/actions/setup-go/blob/5a083d0e9a84784eb32078397cf5459adecb4c40/src/main.ts#L81-L83) sets up a custom problem matcher

  ```typescript
      // add problem matchers
      const matchersPath = path.join(__dirname, '../..', 'matchers.json');
      core.info(`##[add-matcher]${matchersPath}`);
  ```


- [`matchers.json`](https://github.com/actions/setup-go/blob/5a083d0e9a84784eb32078397cf5459adecb4c40/matchers.json) seems like it matches a generic format with nothing like the log-like priority of workflow commands

  ```json
  {
      "problemMatcher": [
          {
              "owner": "go",
              "pattern": [
                  {
                      "regexp": "^\\s*(.+\\.go):(?:(\\d+):(\\d+):)? (.*)",
                      "file": 1,
                      "line": 2,
                      "column": 3,
                      "message": 4
                  }
              ]
          }
      ]
  }
  ```

As for `golangci/golangci-lint-action`, it appears to be designed to conditionally setup problem matchers if `problem-matchers: true` and it has a slightly different setup than `actions/setup-go`:

- [`action.yml`](https://github.com/golangci/golangci-lint-action/blob/b871b4f9de8b829c8d43b0e3bcdf9a8a3f141a3b/action.yml#L45-L48) 

  ```yaml
    problem-matchers:
      description: "Force the usage of the embedded problem matchers"
      default: 'false'
      required: false
  ```

- [`src/run.ts`](https://github.com/golangci/golangci-lint-action/blob/b871b4f9de8b829c8d43b0e3bcdf9a8a3f141a3b/src/run.ts#L68-L77)

  ```typescript
    const problemMatchers = core.getBooleanInput(`problem-matchers`)
  
    if (problemMatchers) {
      const matchersPath = path.join(__dirname, "../..", "problem-matchers.json")
      if (fs.existsSync(matchersPath)) {
        // Adds problem matchers.
        // https://github.com/actions/setup-go/blob/cdcb36043654635271a94b9a6d1392de5bb323a7/src/main.ts#L81-L83
        core.info(`##[add-matcher]${matchersPath}`)
      }
    }
  ```

- [`problem-matchers.json`](https://github.com/golangci/golangci-lint-action/blob/b871b4f9de8b829c8d43b0e3bcdf9a8a3f141a3b/problem-matchers.json)

  ```json
  {
    "problemMatcher": [
      {
        "owner": "golangci-lint-colored-line-number",
        "severity": "error",
        "pattern": [
          {
            "regexp": "^([^:]+):(\\d+):(?:(\\d+):)?\\s+(.+ \\(.+\\))$",
            "file": 1,
            "line": 2,
            "column": 3,
            "message": 4
          }
        ]
      }
    ]
  }
  ```

### Which should we use?

1. `actions/setup-go`

   ```json
   "regexp": "^\\s*(.+\\.go):(?:(\\d+):(\\d+):)? (.*)"
   ```

1. `golangci/golangci-lint-action` with an `error` severity

   ```json
   "regexp": "^([^:]+):(\\d+):(?:(\\d+):)?\\s+(.+ \\(.+\\))$"
   ```

Between the 2, I would assume the problem matcher setup by `golangci` is more appropriate for its use than the generic, non-severity problem matcher from `actions/setup-go`.

![the end](https://github.com/user-attachments/assets/bef3eaae-09ad-427f-b375-cc9c466ecff2)

created_at: 2025-03-11T12:22:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,20 +7,21 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - name: Set up Go
-        uses: actions/setup-go@v3
-        with:
-          go-version: "1.21"
-
       - name: Checkout repository
         uses: actions/checkout@v4
 
+      - name: Set up Go
path: .github/workflows/lint.yml
user: login: andyfeller
body: In order for `actions/setup-go@v5` to use the `go.mod` for determining the Go toolchain version.  These workflows having a hardcoded Go version makes situations where the version changes a little more painful than needed.
created_at: 2025-03-11T12:25:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,7 +72,7 @@ func (m *Map) AddEntry(key string, value *Map) {
 }
 
 func (m *Map) Empty() bool {
-	return m.Content == nil || len(m.Content) == 0
+	return len(m.Content) == 0
path: internal/yamlmap/yaml_map.go
user: login: andyfeller
body: Great question!  This was the only error / warning raised by `golangci-lint`:

```shell
$ golangci-lint run --out-format=colored-line-number
internal/yamlmap/yaml_map.go:75:9: S1009: should omit nil check; len() for nil slices is defined as zero (gosimple)
	return m.Content == nil || len(m.Content) == 0
	       ^
```
created_at: 2025-03-11T12:27:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,106 @@
+package markdown
+
+import (
+	"strconv"
+
+	"github.com/charmbracelet/glamour/ansi"
+	"github.com/charmbracelet/glamour/styles"
+)
+
+type ANSIColorCode int
+
+const (
+	Black ANSIColorCode = iota
+	Red
+	Green
+	Yellow
+	Blue
+	Magenta
+	Cyan
+	White
+	BrightBlack
+	BrightRed
+	BrightGreen
+	BrightYellow
+	BrightBlue
+	BrightMagenta
+	BrightCyan
+	BrightWhite
+)
+
+func (a ANSIColorCode) String() string {
+	return strconv.Itoa(int(a))
+}
+
+func (a ANSIColorCode) StrPtr() *string {
path: pkg/markdown/accessibility.go
user: login: andyfeller
body: Yeah, I believe it was based on 2 ideas:

- [`charmbracelet/glamour` default styles logic](https://github.com/charmbracelet/glamour/blob/4c040b7fd5c023154de93d5c0f789111ea06c82c/styles/styles.go#L135-L343):

  ```go
	  // DarkStyleConfig is the default dark style.
	  DarkStyleConfig = ansi.StyleConfig{
		  Document: ansi.StyleBlock{
			  StylePrimitive: ansi.StylePrimitive{
				  BlockPrefix: "\n",
				  BlockSuffix: "\n",
				  Color:       stringPtr("252"),
			  },
			  Margin: uintPtr(defaultMargin),
		  },
		  BlockQuote: ansi.StyleBlock{
			  StylePrimitive: ansi.StylePrimitive{},
			  Indent:         uintPtr(1),
			  IndentToken:    stringPtr("│ "),
		  },
  ```
  
  which uses a handful of [helper functions](https://github.com/charmbracelet/glamour/blob/4c040b7fd5c023154de93d5c0f789111ea06c82c/styles/styles.go#L678-L680):
  
  ```go
  func boolPtr(b bool) *bool       { return &b }
  func stringPtr(s string) *string { return &s }
  func uintPtr(u uint) *uint       { return &u }
  ```

- [`color.go` in `cli/cli` and `ansi.ColorFunc()`](https://github.com/cli/cli/blob/af4acb380136fd106b38cc0ab404ff975bca9795/pkg/iostreams/color.go#L15-L33)

  This was more inspirational about having a type where information about color could then be leveraged elsewhere rather than the unexported private functions above:

  ```go
  var (
	  magenta            = ansi.ColorFunc("magenta")
	  cyan               = ansi.ColorFunc("cyan")
	  red                = ansi.ColorFunc("red")
	  yellow             = ansi.ColorFunc("yellow")
	  blue               = ansi.ColorFunc("blue")
	  green              = ansi.ColorFunc("green")
	  gray               = ansi.ColorFunc("black+h")
	  lightGrayUnderline = ansi.ColorFunc("white+du")
	  bold               = ansi.ColorFunc("default+b")
	  cyanBold           = ansi.ColorFunc("cyan+b")
	  greenBold          = ansi.ColorFunc("green+b")
	  highlightStart     = ansi.ColorCode(highlightStyle)
	  highlight          = ansi.ColorFunc(highlightStyle)
  
	  gray256 = func(t string) string {
		  return fmt.Sprintf("\x1b[%d;5;%dm%s\x1b[m", 38, 242, t)
	  }
  )
  ```
```
created_at: 2025-03-11T12:44:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,106 @@
+package markdown
path: pkg/markdown/accessibility.go
user: login: andyfeller
body: Great question!  I can get behind that especially as we don't want anyone thinking its stable, yeah.
created_at: 2025-03-11T12:45:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,40 @@
+package color
+
+import (
+	"os"
+
+	"github.com/cli/go-gh/v2/pkg/config"
+)
+
+const (
+	// AccessibleColorsEnv is the name of the environment variable to enable accessibile color features.
+	AccessibleColorsEnv = "GH_ACCESSIBLE_COLORS"
+
+	// AccessibleColorsSetting is the name of the `gh config` setting to enable accessibile color features.
+	AccessibleColorsSetting = "accessible_colors"
+)
+
+// IsAccessibleColorsEnabled returns true if accessible colors are enabled via environment variable
+// or configuration setting with the environment variable having higher precedence.
+//
+// If the environment variable is empty, "0", or "false", the accessible colors are disabled.
+// Any other value enables the accessibility feature.
+//
+// Note this is an experimental feature that is subject to change.
+func IsAccessibleColorsEnabled() bool {
+	envVar := os.Getenv(AccessibleColorsEnv)
+	if envVar != "" {
+		return isEnvVarEnabled(envVar)
+	}
+
+	// We are not handling errors because we don't want to fail if the config is not
+	// read. Instead, we assume an empty configuration is equivalent to "disabled".
+	cfg, _ := config.Read(nil)
+	accessibleConfigValue, _ := cfg.Get([]string{AccessibleColorsSetting})
+
+	return accessibleConfigValue == "enabled"
+}
+
+func isEnvVarEnabled(envVar string) bool {
+	return envVar != "" && envVar != "0" && envVar != "false"
path: pkg/x/color/accessibility.go
user: login: andyfeller
body: Totally fair call out!

I was hoping we could tie into existing logic as this work evolved as this shouldn't be a new problem, but I suppose we don't really:

https://github.com/cli/go-gh/blob/7a6e5543a7f13a372dfafa88fac99981496b4991/pkg/api/http_client.go#L80-L88
created_at: 2025-03-11T12:51:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,40 @@
+package color
+
+import (
+	"os"
+
+	"github.com/cli/go-gh/v2/pkg/config"
+)
+
+const (
+	// AccessibleColorsEnv is the name of the environment variable to enable accessibile color features.
+	AccessibleColorsEnv = "GH_ACCESSIBLE_COLORS"
+
+	// AccessibleColorsSetting is the name of the `gh config` setting to enable accessibile color features.
+	AccessibleColorsSetting = "accessible_colors"
+)
+
+// IsAccessibleColorsEnabled returns true if accessible colors are enabled via environment variable
+// or configuration setting with the environment variable having higher precedence.
+//
+// If the environment variable is empty, "0", or "false", the accessible colors are disabled.
+// Any other value enables the accessibility feature.
+//
+// Note this is an experimental feature that is subject to change.
+func IsAccessibleColorsEnabled() bool {
+	envVar := os.Getenv(AccessibleColorsEnv)
+	if envVar != "" {
+		return isEnvVarEnabled(envVar)
+	}
+
+	// We are not handling errors because we don't want to fail if the config is not
+	// read. Instead, we assume an empty configuration is equivalent to "disabled".
+	cfg, _ := config.Read(nil)
+	accessibleConfigValue, _ := cfg.Get([]string{AccessibleColorsSetting})
+
+	return accessibleConfigValue == "enabled"
+}
+
+func isEnvVarEnabled(envVar string) bool {
+	return envVar != "" && envVar != "0" && envVar != "false"
path: pkg/x/color/accessibility.go
user: login: andyfeller
body: We probably also want to add `no`, too
created_at: 2025-03-11T12:52:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,20 +7,21 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - name: Set up Go
-        uses: actions/setup-go@v3
-        with:
-          go-version: "1.21"
-
       - name: Checkout repository
         uses: actions/checkout@v4
 
+      - name: Set up Go
+        uses: actions/setup-go@v5
+        with:
+          go-version-file: go.mod
+
       - name: Check dependencies
         run: |
           go mod tidy
           git diff --exit-code go.mod
 
       - name: Lint
-        uses: golangci/golangci-lint-action@v3.4.0
+        uses: golangci/golangci-lint-action@v6
         with:
-          version: v1.54
+          version: v1.64
+          problem-matchers: true
path: .github/workflows/lint.yml
user: login: williammartin
body: wat
created_at: 2025-03-11T13:35:24Z
author_association: MEMBER
diff_hunk: @@ -7,20 +7,21 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - name: Set up Go
-        uses: actions/setup-go@v3
-        with:
-          go-version: "1.21"
-
       - name: Checkout repository
         uses: actions/checkout@v4
 
+      - name: Set up Go
+        uses: actions/setup-go@v5
+        with:
+          go-version-file: go.mod
+
       - name: Check dependencies
         run: |
           go mod tidy
           git diff --exit-code go.mod
 
       - name: Lint
-        uses: golangci/golangci-lint-action@v3.4.0
+        uses: golangci/golangci-lint-action@v6
         with:
-          version: v1.54
+          version: v1.64
+          problem-matchers: true
path: .github/workflows/lint.yml
user: login: williammartin
body: Thanks for the explanation. I guess at some point it would be good to see a comparison of the two visually.
created_at: 2025-03-11T13:37:05Z
author_association: MEMBER
diff_hunk: @@ -7,20 +7,21 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - name: Set up Go
-        uses: actions/setup-go@v3
-        with:
-          go-version: "1.21"
-
       - name: Checkout repository
         uses: actions/checkout@v4
 
+      - name: Set up Go
path: .github/workflows/lint.yml
user: login: williammartin
body: Got it, thanks!
created_at: 2025-03-11T13:38:08Z
author_association: MEMBER
diff_hunk: @@ -72,7 +72,7 @@ func (m *Map) AddEntry(key string, value *Map) {
 }
 
 func (m *Map) Empty() bool {
-	return m.Content == nil || len(m.Content) == 0
+	return len(m.Content) == 0
path: internal/yamlmap/yaml_map.go
user: login: williammartin
body: Cool, thanks.
created_at: 2025-03-11T13:38:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,106 @@
+package markdown
+
+import (
+	"strconv"
+
+	"github.com/charmbracelet/glamour/ansi"
+	"github.com/charmbracelet/glamour/styles"
+)
+
+type ANSIColorCode int
+
+const (
+	Black ANSIColorCode = iota
+	Red
+	Green
+	Yellow
+	Blue
+	Magenta
+	Cyan
+	White
+	BrightBlack
+	BrightRed
+	BrightGreen
+	BrightYellow
+	BrightBlue
+	BrightMagenta
+	BrightCyan
+	BrightWhite
+)
+
+func (a ANSIColorCode) String() string {
+	return strconv.Itoa(int(a))
+}
+
+func (a ANSIColorCode) StrPtr() *string {
path: pkg/markdown/accessibility.go
user: login: williammartin
body: Alright, makes sense to me.

Maybe it doesn't need to be exported?
created_at: 2025-03-11T13:39:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,92 @@
+package color
+
+import (
+	"testing"
+
+	"github.com/MakeNowJust/heredoc"
+	"github.com/cli/go-gh/v2/internal/testutils"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIsAccessibleColorsEnabled(t *testing.T) {
+	tests := []struct {
+		name        string
+		envVarValue string
+		cfgStr      string
+		wantOut     bool
+	}{
+		{
+			name:        "When the accessibility configuration and env var are both unset, it should return false",
+			envVarValue: "",
+			cfgStr:      "",
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is unset but the env var is set to something truthy (not '0' or 'false'), it should return true",
+			envVarValue: "1",
+			cfgStr:      "",
+			wantOut:     true,
+		},
+		{
+			name:        "When the accessibility configuration is unset and the env var returns '0', it should return false",
+			envVarValue: "0",
+			cfgStr:      "",
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is unset and the env var returns 'false', it should return false",
+			envVarValue: "false",
+			cfgStr:      "",
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is set to enabled and the env var is unset, it should return true",
+			envVarValue: "",
+			cfgStr:      accessibilityEnabledConfig(),
+			wantOut:     true,
+		},
+		{
+			name:        "When the accessibility configuration is set to disabled and the env var is unset, it should return false",
+			envVarValue: "",
+			cfgStr:      accessibilityDisabledConfig(),
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is set to disabled and the env var is set to something truthy (not '0' or 'false'), it should return true",
+			envVarValue: "true",
+			cfgStr:      accessibilityDisabledConfig(),
+			wantOut:     true,
+		},
+		{
+			name:        "When the accessibility configuration is set to enabled and the env var is set to '0', it should return false",
+			envVarValue: "0",
+			cfgStr:      accessibilityEnabledConfig(),
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is set to enabled and the env var is set to 'false', it should return false",
+			envVarValue: "false",
+			cfgStr:      accessibilityEnabledConfig(),
+			wantOut:     false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Setenv("GH_ACCESSIBLE_COLORS", tt.envVarValue)
path: pkg/x/color/accessibility_test.go
user: login: williammartin
body: **Food For Thought**

In practice I think this will work but I'm not sure it's totally correct in line with the descriptions e.g. the first test says:

```
When the accessibility configuration and env var are both unset, it should return false
```

But setting an env var to an empty string is not the same as it being unset. You can confirm this with:

```go
			_, set := os.LookupEnv("GH_ACCESSIBLE_COLORS")
			fmt.Println(set)
```
created_at: 2025-03-11T14:20:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,92 @@
+package color
+
+import (
+	"testing"
+
+	"github.com/MakeNowJust/heredoc"
+	"github.com/cli/go-gh/v2/internal/testutils"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIsAccessibleColorsEnabled(t *testing.T) {
+	tests := []struct {
+		name        string
+		envVarValue string
+		cfgStr      string
+		wantOut     bool
+	}{
+		{
+			name:        "When the accessibility configuration and env var are both unset, it should return false",
+			envVarValue: "",
+			cfgStr:      "",
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is unset but the env var is set to something truthy (not '0' or 'false'), it should return true",
+			envVarValue: "1",
+			cfgStr:      "",
+			wantOut:     true,
+		},
+		{
+			name:        "When the accessibility configuration is unset and the env var returns '0', it should return false",
+			envVarValue: "0",
+			cfgStr:      "",
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is unset and the env var returns 'false', it should return false",
+			envVarValue: "false",
+			cfgStr:      "",
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is set to enabled and the env var is unset, it should return true",
+			envVarValue: "",
+			cfgStr:      accessibilityEnabledConfig(),
+			wantOut:     true,
+		},
+		{
+			name:        "When the accessibility configuration is set to disabled and the env var is unset, it should return false",
+			envVarValue: "",
+			cfgStr:      accessibilityDisabledConfig(),
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is set to disabled and the env var is set to something truthy (not '0' or 'false'), it should return true",
+			envVarValue: "true",
+			cfgStr:      accessibilityDisabledConfig(),
+			wantOut:     true,
+		},
+		{
+			name:        "When the accessibility configuration is set to enabled and the env var is set to '0', it should return false",
+			envVarValue: "0",
+			cfgStr:      accessibilityEnabledConfig(),
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is set to enabled and the env var is set to 'false', it should return false",
+			envVarValue: "false",
+			cfgStr:      accessibilityEnabledConfig(),
+			wantOut:     false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Setenv("GH_ACCESSIBLE_COLORS", tt.envVarValue)
path: pkg/x/color/accessibility_test.go
user: login: williammartin
body: Actually if you look at this test:

```go
		{
			name:        "When the accessibility configuration is set to disabled and the env var is unset, it should return false",
			envVarValue: "",
			cfgStr:      accessibilityDisabledConfig(),
			wantOut:     false,
		},
```

You might expect that changing the implementation of `IsAccessibleColorsEnabled` with a refactor to a true "set" or "unset" check would be safe:

```go
	envVar, set := os.LookupEnv(AccessibleColorsEnv)
	if set {
		return isEnvVarEnabled(envVar)
	}
```

But it's not because `""` is considered set. This refactor actually results in the config **never** being checked in the tests.

For me, this is enough to **Request Changes** to make it clear what an empty string vs unset should do.
created_at: 2025-03-11T14:26:07Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,92 @@
+package color
+
+import (
+	"testing"
+
+	"github.com/MakeNowJust/heredoc"
+	"github.com/cli/go-gh/v2/internal/testutils"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIsAccessibleColorsEnabled(t *testing.T) {
+	tests := []struct {
+		name        string
+		envVarValue string
+		cfgStr      string
+		wantOut     bool
+	}{
+		{
+			name:        "When the accessibility configuration and env var are both unset, it should return false",
+			envVarValue: "",
+			cfgStr:      "",
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is unset but the env var is set to something truthy (not '0' or 'false'), it should return true",
+			envVarValue: "1",
+			cfgStr:      "",
+			wantOut:     true,
+		},
+		{
+			name:        "When the accessibility configuration is unset and the env var returns '0', it should return false",
+			envVarValue: "0",
+			cfgStr:      "",
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is unset and the env var returns 'false', it should return false",
+			envVarValue: "false",
+			cfgStr:      "",
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is set to enabled and the env var is unset, it should return true",
+			envVarValue: "",
+			cfgStr:      accessibilityEnabledConfig(),
+			wantOut:     true,
+		},
+		{
+			name:        "When the accessibility configuration is set to disabled and the env var is unset, it should return false",
+			envVarValue: "",
+			cfgStr:      accessibilityDisabledConfig(),
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is set to disabled and the env var is set to something truthy (not '0' or 'false'), it should return true",
+			envVarValue: "true",
+			cfgStr:      accessibilityDisabledConfig(),
+			wantOut:     true,
+		},
+		{
+			name:        "When the accessibility configuration is set to enabled and the env var is set to '0', it should return false",
+			envVarValue: "0",
+			cfgStr:      accessibilityEnabledConfig(),
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is set to enabled and the env var is set to 'false', it should return false",
+			envVarValue: "false",
+			cfgStr:      accessibilityEnabledConfig(),
+			wantOut:     false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Setenv("GH_ACCESSIBLE_COLORS", tt.envVarValue)
+			testutils.StubConfig(t, tt.cfgStr)
+			assert.Equal(t, tt.wantOut, IsAccessibleColorsEnabled())
+		})
+	}
+}
+
+func accessibilityEnabledConfig() string {
+	return heredoc.Docf(`
+		%s: enabled
+	`, AccessibleColorsSetting)
path: pkg/x/color/accessibility_test.go
user: login: williammartin
body: **Nit**

I understand your desire to DRY out the use of this config but I have mixed feelings about it. The thing is, the tests are supposed to act as guardrails but changing the value of `AccessibleColorsSetting` would break users but wouldn't fail the tests.

I think it's generally better to duplicate in tests for this reason.
created_at: 2025-03-11T14:39:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,92 @@
+package color
+
+import (
+	"testing"
+
+	"github.com/MakeNowJust/heredoc"
+	"github.com/cli/go-gh/v2/internal/testutils"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIsAccessibleColorsEnabled(t *testing.T) {
+	tests := []struct {
+		name        string
+		envVarValue string
+		cfgStr      string
+		wantOut     bool
+	}{
+		{
+			name:        "When the accessibility configuration and env var are both unset, it should return false",
+			envVarValue: "",
+			cfgStr:      "",
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is unset but the env var is set to something truthy (not '0' or 'false'), it should return true",
+			envVarValue: "1",
+			cfgStr:      "",
+			wantOut:     true,
+		},
+		{
+			name:        "When the accessibility configuration is unset and the env var returns '0', it should return false",
+			envVarValue: "0",
+			cfgStr:      "",
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is unset and the env var returns 'false', it should return false",
+			envVarValue: "false",
+			cfgStr:      "",
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is set to enabled and the env var is unset, it should return true",
+			envVarValue: "",
+			cfgStr:      accessibilityEnabledConfig(),
+			wantOut:     true,
+		},
+		{
+			name:        "When the accessibility configuration is set to disabled and the env var is unset, it should return false",
+			envVarValue: "",
+			cfgStr:      accessibilityDisabledConfig(),
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is set to disabled and the env var is set to something truthy (not '0' or 'false'), it should return true",
+			envVarValue: "true",
+			cfgStr:      accessibilityDisabledConfig(),
+			wantOut:     true,
+		},
+		{
+			name:        "When the accessibility configuration is set to enabled and the env var is set to '0', it should return false",
+			envVarValue: "0",
+			cfgStr:      accessibilityEnabledConfig(),
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is set to enabled and the env var is set to 'false', it should return false",
+			envVarValue: "false",
+			cfgStr:      accessibilityEnabledConfig(),
+			wantOut:     false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Setenv("GH_ACCESSIBLE_COLORS", tt.envVarValue)
+			testutils.StubConfig(t, tt.cfgStr)
+			assert.Equal(t, tt.wantOut, IsAccessibleColorsEnabled())
+		})
+	}
+}
+
+func accessibilityEnabledConfig() string {
+	return heredoc.Docf(`
+		%s: enabled
+	`, AccessibleColorsSetting)
path: pkg/x/color/accessibility_test.go
user: login: williammartin
body: In fact you avoided this trap for the envvar by using the string directly in the test.
created_at: 2025-03-11T14:47:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,118 @@
+package markdown
+
+import (
+	"testing"
+
+	"github.com/charmbracelet/glamour/ansi"
+	"github.com/charmbracelet/glamour/styles"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestANSIColorCodeString(t *testing.T) {
path: pkg/markdown/accessibility_test.go
user: login: williammartin
body: **Question**

Maybe this is pedantic but it feels like we should test all the colors here. I swapped the order of some of them and no tests failed.

Maybe it's unnecessary, who's really going to swap the order? Maybe a comment would be fine. I don't think it's obvious to uninformed readers that the order of the enumeration is important.

An alternative would be to use a map to `int`.

What do you think?
created_at: 2025-03-11T14:54:28Z
author_association: MEMBER
diff_hunk: @@ -33,9 +34,16 @@ func WithWrap(w int) glamour.TermRendererOption {
 // If the environment variable GLAMOUR_STYLE is set, it will take precedence over the provided theme.
 func WithTheme(theme string) glamour.TermRendererOption {
 	style := os.Getenv("GLAMOUR_STYLE")
+	accessible := color.IsAccessibleColorsEnabled()
path: pkg/markdown/markdown.go
user: login: williammartin
body: **Nit**

If this is only used inside the `light`, `dark` case then maybe just inline it in the `if` there?

It would probably be a totally negligible performance improvement but generally I value the scoping being tight.
created_at: 2025-03-11T16:00:21Z
author_association: MEMBER
diff_hunk: @@ -1,38 +1,282 @@
 package markdown
 
 import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
 	"testing"
 
+	"github.com/MakeNowJust/heredoc"
+	"github.com/alecthomas/chroma/v2/styles"
+	"github.com/cli/go-gh/v2/pkg/x/color"
+	ansi "github.com/leaanthony/go-ansi-parser"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
-func Test_Render(t *testing.T) {
+// glamour theme colors found at https://github.com/charmbracelet/glamour/tree/master/styles
+const (
+	glamourLightH2_8bitColorSeq = "\x1b[38;5;27;"
+	glamourDarkH2_8bitColorSeq  = "\x1b[38;5;39;"
+	customH2_8bitColorSeq       = "\x1b[38;5;61;"
+	magenta_4bitColorSeq        = "\x1b[35;"
+	brightMagenta_4bitColorSeq  = "\x1b[95;"
+)
+
+// Test_RenderAccessible tests rendered markdown for accessibility concerns such as color mode / depth and other display attributes.
+// It works by parsing the rendered markdown for ANSI escape sequences and checking their display attributes.
+// Test scenarios allow multiple color mode / depths because `ansi.Parse()` considers `\x1b[0m` sequence as part of `ansi.Default`.
+func Test_RenderAccessible(t *testing.T) {
path: pkg/markdown/markdown_test.go
user: login: williammartin
body: **Praise**

This test is sick.
created_at: 2025-03-11T16:00:44Z
author_association: MEMBER
diff_hunk: @@ -1,38 +1,282 @@
 package markdown
 
 import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
 	"testing"
 
+	"github.com/MakeNowJust/heredoc"
+	"github.com/alecthomas/chroma/v2/styles"
+	"github.com/cli/go-gh/v2/pkg/x/color"
+	ansi "github.com/leaanthony/go-ansi-parser"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
-func Test_Render(t *testing.T) {
+// glamour theme colors found at https://github.com/charmbracelet/glamour/tree/master/styles
+const (
+	glamourLightH2_8bitColorSeq = "\x1b[38;5;27;"
+	glamourDarkH2_8bitColorSeq  = "\x1b[38;5;39;"
+	customH2_8bitColorSeq       = "\x1b[38;5;61;"
+	magenta_4bitColorSeq        = "\x1b[35;"
+	brightMagenta_4bitColorSeq  = "\x1b[95;"
+)
+
+// Test_RenderAccessible tests rendered markdown for accessibility concerns such as color mode / depth and other display attributes.
+// It works by parsing the rendered markdown for ANSI escape sequences and checking their display attributes.
+// Test scenarios allow multiple color mode / depths because `ansi.Parse()` considers `\x1b[0m` sequence as part of `ansi.Default`.
+func Test_RenderAccessible(t *testing.T) {
+	goCodeBlock := heredoc.Docf(`
+		%[1]s%[1]s%[1]sgo
+		package main
+
+		import (
+			"fmt"
+		)
+
+		func main() {
+			fmt.Println("Hello, world!")
+		}
+		%[1]s%[1]s%[1]s
+	`, "`")
+
+	shellCodeBlock := heredoc.Docf(`
+		%[1]s%[1]s%[1]sshell
+		# list all repositories for a user
+		$ gh api graphql --paginate -f query='
+			query($endCursor: String) {
+				viewer {
+					repositories(first: 100, after: $endCursor) {
+						nodes { nameWithOwner }
+						pageInfo {
+							hasNextPage
+							endCursor
+						}
+					}
+				}
+			}
+		'
+		%[1]s%[1]s%[1]s
+	`)
+
+	tests := []struct {
+		name              string
+		text              string
+		theme             string
+		accessible        bool
+		wantColourModes   []ansi.ColourMode
+		allowDimFaintText bool
+	}{
+		{
+			name:              "when the light theme is selected, the Go codeblock renders using 8-bit colors",
+			text:              goCodeBlock,
+			theme:             "light",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the dark theme is selected, the Go codeblock renders using 8-bit colors",
+			text:              goCodeBlock,
+			theme:             "dark",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the accessible env var is set and the light theme is selected, the Go codeblock renders using 4-bit colors without dim/faint text",
+			text:              goCodeBlock,
+			theme:             "light",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		{
+			name:              "when the accessible env var is set and the dark theme is selected, the Go codeblock renders using 4-bit colors without dim/faint text",
+			text:              goCodeBlock,
+			theme:             "dark",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		{
+			name:              "when the light theme is selected, the Shell codeblock renders using 8-bit colors",
+			text:              shellCodeBlock,
+			theme:             "light",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the dark theme is selected, the Shell codeblock renders using 8-bit colors",
+			text:              shellCodeBlock,
+			theme:             "dark",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the accessible env var is set and the light theme is selected, the Shell codeblock renders using 4-bit colors without dim/faint text",
+			text:              shellCodeBlock,
+			theme:             "light",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		{
+			name:              "when the accessible env var is set and the dark theme is selected, the Shell codeblock renders using 4-bit colors without dim/faint text",
+			text:              shellCodeBlock,
+			theme:             "dark",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.accessible {
+				t.Setenv(color.AccessibleColorsEnv, "true")
+			}
+
+			out, err := Render(tt.text, WithTheme(tt.theme))
+			require.NoError(t, err)
+
+			// Parse and test ANSI escape sequences from rendered markdown for inaccessible display attributes
+			styledText, err := ansi.Parse(out)
+			require.NoError(t, err)
+
+			for _, st := range styledText {
+				require.Containsf(t, tt.wantColourModes, st.ColourMode, "Unexpected color mode detected in '%s' at %d", st, st.Offset)
+
+				if st.Faint() {
+					require.Truef(t, tt.allowDimFaintText, "Unexpected dim/faint text detected in '%s' at %d", st, st.Offset)
+				}
+			}
+		})
+	}
+}
+
+// Test_RenderColor verifies that the proper ANSI color codes are applied to the rendered
+// markdown by examining the ANSI escape sequences in the output for the correct color
+// match. For more information on ANSI color codes, see
+// https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
+func Test_RenderColor(t *testing.T) {
 	t.Setenv("GLAMOUR_STYLE", "")
+
+	codeBlock := heredoc.Docf(`
+		%[1]s%[1]s%[1]sgo
+		fmt.Println("Hello, world!")
+		%[1]s%[1]s%[1]s
+	`, "`")
+
 	tests := []struct {
-		name  string
-		text  string
-		theme string
+		name             string
+		text             string
+		theme            string
+		styleEnvVar      string
+		accessibleEnvVar string
+		wantOut          string
 	}{
 		{
-			name:  "light style",
-			text:  "some text",
-			theme: "light",
+			name:    "when the light theme is selected, the h2 renders using the 8-bit blue 27 provided by glamour",
+			text:    "## h2",
+			theme:   "light",
+			wantOut: fmt.Sprintf("%s1mh2", glamourLightH2_8bitColorSeq),
+		},
+		{
+			name:    "when the dark theme is selected, the h2 renders using the 8-bit blue 39 provided by glamour",
+			text:    "## h2",
+			theme:   "dark",
+			wantOut: fmt.Sprintf("%s1mh2", glamourDarkH2_8bitColorSeq),
+		},
+		{
+			name:    "when no theme is selected, the h2 renders in plain text without ansi coloring",
+			text:    "## h2",
+			theme:   "none",
+			wantOut: "## h2",
+		},
+		{
+			name:        "when the style env var is set, we override the theme with that style",
+			text:        "## h2",
+			theme:       "light",
+			styleEnvVar: "customStyle",
+			wantOut:     fmt.Sprintf("%s1mh2", customH2_8bitColorSeq),
+		},
+		{
+			name:             "when the accessible env var is set and the light theme is selected, the h2 renders using the 4-bit magenta provided by the light accessible style",
+			text:             "## h2",
+			theme:            "light",
+			accessibleEnvVar: "true",
+			wantOut:          fmt.Sprintf("%s1mh2", magenta_4bitColorSeq),
+		},
+		{
+			name:             "when the accessible env var is set and the dark theme is selected, the h2 renders using the 4-bit bright magenta provided by the dark accessible style",
+			text:             "## h2",
+			theme:            "dark",
+			accessibleEnvVar: "true",
+			wantOut:          fmt.Sprintf("%s1mh2", brightMagenta_4bitColorSeq),
+		},
+		{
+			name:    "when the light theme is selected, the codeblock renders using 8-bit colors",
+			text:    codeBlock,
+			theme:   "light",
+			wantOut: "\x1b[0m\x1b[38;5;235mfmt\x1b[0m\x1b[38;5;210m.\x1b[0m\x1b[38;5;35mPrintln\x1b[0m\x1b[38;5;210m(\x1b[0m\x1b[38;5;95m\"Hello, world!\"\x1b[0m\x1b[38;5;210m)\x1b[0m",
 		},
 		{
-			name:  "dark style",
-			text:  "some text",
-			theme: "dark",
+			name:    "when the dark theme is selected, the codeblock renders using 8-bit colors",
+			text:    codeBlock,
+			theme:   "dark",
+			wantOut: "\x1b[0m\x1b[38;5;251mfmt\x1b[0m\x1b[38;5;187m.\x1b[0m\x1b[38;5;42mPrintln\x1b[0m\x1b[38;5;187m(\x1b[0m\x1b[38;5;173m\"Hello, world!\"\x1b[0m\x1b[38;5;187m)\x1b[0m",
 		},
 		{
-			name:  "notty style",
-			text:  "some text",
-			theme: "none",
+			name:             "when the accessible env var is set and the light theme is selected, the codeblock renders using 4-bit colors",
+			text:             codeBlock,
+			theme:            "light",
+			accessibleEnvVar: "true",
+			wantOut:          "\x1b[0m\x1b[30mfmt\x1b[0m\x1b[33m.\x1b[0m\x1b[36mPrintln\x1b[0m\x1b[33m(\x1b[0m\x1b[90m\"Hello, world!\"\x1b[0m\x1b[33m)\x1b[0m",
+		},
+		{
+			name:             "when the accessible env var is set and the dark theme is selected, the codeblock renders using 4-bit colors",
+			text:             codeBlock,
+			theme:            "dark",
+			accessibleEnvVar: "true",
+			wantOut:          "\x1b[0m\x1b[37mfmt\x1b[0m\x1b[37m.\x1b[0m\x1b[36mPrintln\x1b[0m\x1b[37m(\x1b[0m\x1b[33m\"Hello, world!\"\x1b[0m\x1b[37m)\x1b[0m",
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			_, err := Render(tt.text, WithTheme(tt.theme))
-			assert.NoError(t, err)
+			t.Cleanup(func() {
+				// Chroma caches charm style used to render codeblocks, it must be unregistered to avoid previously used style being reused.
path: pkg/markdown/markdown_test.go
user: login: williammartin
body: **Praise**

Great comment.
created_at: 2025-03-11T16:03:34Z
author_association: MEMBER
diff_hunk: @@ -1,38 +1,282 @@
 package markdown
 
 import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
 	"testing"
 
+	"github.com/MakeNowJust/heredoc"
+	"github.com/alecthomas/chroma/v2/styles"
+	"github.com/cli/go-gh/v2/pkg/x/color"
+	ansi "github.com/leaanthony/go-ansi-parser"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
-func Test_Render(t *testing.T) {
+// glamour theme colors found at https://github.com/charmbracelet/glamour/tree/master/styles
+const (
+	glamourLightH2_8bitColorSeq = "\x1b[38;5;27;"
+	glamourDarkH2_8bitColorSeq  = "\x1b[38;5;39;"
+	customH2_8bitColorSeq       = "\x1b[38;5;61;"
+	magenta_4bitColorSeq        = "\x1b[35;"
+	brightMagenta_4bitColorSeq  = "\x1b[95;"
+)
+
+// Test_RenderAccessible tests rendered markdown for accessibility concerns such as color mode / depth and other display attributes.
+// It works by parsing the rendered markdown for ANSI escape sequences and checking their display attributes.
+// Test scenarios allow multiple color mode / depths because `ansi.Parse()` considers `\x1b[0m` sequence as part of `ansi.Default`.
+func Test_RenderAccessible(t *testing.T) {
+	goCodeBlock := heredoc.Docf(`
+		%[1]s%[1]s%[1]sgo
+		package main
+
+		import (
+			"fmt"
+		)
+
+		func main() {
+			fmt.Println("Hello, world!")
+		}
+		%[1]s%[1]s%[1]s
+	`, "`")
+
+	shellCodeBlock := heredoc.Docf(`
+		%[1]s%[1]s%[1]sshell
+		# list all repositories for a user
+		$ gh api graphql --paginate -f query='
+			query($endCursor: String) {
+				viewer {
+					repositories(first: 100, after: $endCursor) {
+						nodes { nameWithOwner }
+						pageInfo {
+							hasNextPage
+							endCursor
+						}
+					}
+				}
+			}
+		'
+		%[1]s%[1]s%[1]s
+	`)
+
+	tests := []struct {
+		name              string
+		text              string
+		theme             string
+		accessible        bool
+		wantColourModes   []ansi.ColourMode
+		allowDimFaintText bool
+	}{
+		{
+			name:              "when the light theme is selected, the Go codeblock renders using 8-bit colors",
+			text:              goCodeBlock,
+			theme:             "light",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the dark theme is selected, the Go codeblock renders using 8-bit colors",
+			text:              goCodeBlock,
+			theme:             "dark",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the accessible env var is set and the light theme is selected, the Go codeblock renders using 4-bit colors without dim/faint text",
+			text:              goCodeBlock,
+			theme:             "light",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		{
+			name:              "when the accessible env var is set and the dark theme is selected, the Go codeblock renders using 4-bit colors without dim/faint text",
+			text:              goCodeBlock,
+			theme:             "dark",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		{
+			name:              "when the light theme is selected, the Shell codeblock renders using 8-bit colors",
+			text:              shellCodeBlock,
+			theme:             "light",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the dark theme is selected, the Shell codeblock renders using 8-bit colors",
+			text:              shellCodeBlock,
+			theme:             "dark",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the accessible env var is set and the light theme is selected, the Shell codeblock renders using 4-bit colors without dim/faint text",
+			text:              shellCodeBlock,
+			theme:             "light",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		{
+			name:              "when the accessible env var is set and the dark theme is selected, the Shell codeblock renders using 4-bit colors without dim/faint text",
+			text:              shellCodeBlock,
+			theme:             "dark",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.accessible {
+				t.Setenv(color.AccessibleColorsEnv, "true")
+			}
+
+			out, err := Render(tt.text, WithTheme(tt.theme))
+			require.NoError(t, err)
+
+			// Parse and test ANSI escape sequences from rendered markdown for inaccessible display attributes
+			styledText, err := ansi.Parse(out)
+			require.NoError(t, err)
+
+			for _, st := range styledText {
+				require.Containsf(t, tt.wantColourModes, st.ColourMode, "Unexpected color mode detected in '%s' at %d", st, st.Offset)
+
+				if st.Faint() {
+					require.Truef(t, tt.allowDimFaintText, "Unexpected dim/faint text detected in '%s' at %d", st, st.Offset)
+				}
+			}
+		})
+	}
+}
+
+// Test_RenderColor verifies that the proper ANSI color codes are applied to the rendered
+// markdown by examining the ANSI escape sequences in the output for the correct color
+// match. For more information on ANSI color codes, see
+// https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
+func Test_RenderColor(t *testing.T) {
 	t.Setenv("GLAMOUR_STYLE", "")
+
+	codeBlock := heredoc.Docf(`
+		%[1]s%[1]s%[1]sgo
+		fmt.Println("Hello, world!")
+		%[1]s%[1]s%[1]s
+	`, "`")
+
 	tests := []struct {
-		name  string
-		text  string
-		theme string
+		name             string
+		text             string
+		theme            string
+		styleEnvVar      string
+		accessibleEnvVar string
+		wantOut          string
 	}{
 		{
-			name:  "light style",
-			text:  "some text",
-			theme: "light",
+			name:    "when the light theme is selected, the h2 renders using the 8-bit blue 27 provided by glamour",
+			text:    "## h2",
+			theme:   "light",
+			wantOut: fmt.Sprintf("%s1mh2", glamourLightH2_8bitColorSeq),
+		},
+		{
+			name:    "when the dark theme is selected, the h2 renders using the 8-bit blue 39 provided by glamour",
+			text:    "## h2",
+			theme:   "dark",
+			wantOut: fmt.Sprintf("%s1mh2", glamourDarkH2_8bitColorSeq),
+		},
+		{
+			name:    "when no theme is selected, the h2 renders in plain text without ansi coloring",
+			text:    "## h2",
+			theme:   "none",
+			wantOut: "## h2",
+		},
+		{
+			name:        "when the style env var is set, we override the theme with that style",
+			text:        "## h2",
+			theme:       "light",
+			styleEnvVar: "customStyle",
+			wantOut:     fmt.Sprintf("%s1mh2", customH2_8bitColorSeq),
+		},
+		{
+			name:             "when the accessible env var is set and the light theme is selected, the h2 renders using the 4-bit magenta provided by the light accessible style",
+			text:             "## h2",
+			theme:            "light",
+			accessibleEnvVar: "true",
+			wantOut:          fmt.Sprintf("%s1mh2", magenta_4bitColorSeq),
+		},
+		{
+			name:             "when the accessible env var is set and the dark theme is selected, the h2 renders using the 4-bit bright magenta provided by the dark accessible style",
+			text:             "## h2",
+			theme:            "dark",
+			accessibleEnvVar: "true",
+			wantOut:          fmt.Sprintf("%s1mh2", brightMagenta_4bitColorSeq),
+		},
+		{
+			name:    "when the light theme is selected, the codeblock renders using 8-bit colors",
+			text:    codeBlock,
+			theme:   "light",
+			wantOut: "\x1b[0m\x1b[38;5;235mfmt\x1b[0m\x1b[38;5;210m.\x1b[0m\x1b[38;5;35mPrintln\x1b[0m\x1b[38;5;210m(\x1b[0m\x1b[38;5;95m\"Hello, world!\"\x1b[0m\x1b[38;5;210m)\x1b[0m",
 		},
 		{
-			name:  "dark style",
-			text:  "some text",
-			theme: "dark",
+			name:    "when the dark theme is selected, the codeblock renders using 8-bit colors",
+			text:    codeBlock,
+			theme:   "dark",
+			wantOut: "\x1b[0m\x1b[38;5;251mfmt\x1b[0m\x1b[38;5;187m.\x1b[0m\x1b[38;5;42mPrintln\x1b[0m\x1b[38;5;187m(\x1b[0m\x1b[38;5;173m\"Hello, world!\"\x1b[0m\x1b[38;5;187m)\x1b[0m",
 		},
 		{
-			name:  "notty style",
-			text:  "some text",
-			theme: "none",
+			name:             "when the accessible env var is set and the light theme is selected, the codeblock renders using 4-bit colors",
+			text:             codeBlock,
+			theme:            "light",
+			accessibleEnvVar: "true",
+			wantOut:          "\x1b[0m\x1b[30mfmt\x1b[0m\x1b[33m.\x1b[0m\x1b[36mPrintln\x1b[0m\x1b[33m(\x1b[0m\x1b[90m\"Hello, world!\"\x1b[0m\x1b[33m)\x1b[0m",
+		},
+		{
+			name:             "when the accessible env var is set and the dark theme is selected, the codeblock renders using 4-bit colors",
+			text:             codeBlock,
+			theme:            "dark",
+			accessibleEnvVar: "true",
+			wantOut:          "\x1b[0m\x1b[37mfmt\x1b[0m\x1b[37m.\x1b[0m\x1b[36mPrintln\x1b[0m\x1b[37m(\x1b[0m\x1b[33m\"Hello, world!\"\x1b[0m\x1b[37m)\x1b[0m",
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			_, err := Render(tt.text, WithTheme(tt.theme))
-			assert.NoError(t, err)
+			t.Cleanup(func() {
+				// Chroma caches charm style used to render codeblocks, it must be unregistered to avoid previously used style being reused.
+				delete(styles.Registry, "charm")
+			})
+			t.Setenv(color.AccessibleColorsEnv, tt.accessibleEnvVar)
path: pkg/markdown/markdown_test.go
user: login: williammartin
body: **Nit**

Same issue as DRYing the config setting mentioned in another comment.
created_at: 2025-03-11T16:04:22Z
author_association: MEMBER
diff_hunk: @@ -1,38 +1,282 @@
 package markdown
 
 import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
 	"testing"
 
+	"github.com/MakeNowJust/heredoc"
+	"github.com/alecthomas/chroma/v2/styles"
+	"github.com/cli/go-gh/v2/pkg/x/color"
+	ansi "github.com/leaanthony/go-ansi-parser"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
-func Test_Render(t *testing.T) {
+// glamour theme colors found at https://github.com/charmbracelet/glamour/tree/master/styles
+const (
+	glamourLightH2_8bitColorSeq = "\x1b[38;5;27;"
+	glamourDarkH2_8bitColorSeq  = "\x1b[38;5;39;"
+	customH2_8bitColorSeq       = "\x1b[38;5;61;"
+	magenta_4bitColorSeq        = "\x1b[35;"
+	brightMagenta_4bitColorSeq  = "\x1b[95;"
+)
+
+// Test_RenderAccessible tests rendered markdown for accessibility concerns such as color mode / depth and other display attributes.
+// It works by parsing the rendered markdown for ANSI escape sequences and checking their display attributes.
+// Test scenarios allow multiple color mode / depths because `ansi.Parse()` considers `\x1b[0m` sequence as part of `ansi.Default`.
+func Test_RenderAccessible(t *testing.T) {
+	goCodeBlock := heredoc.Docf(`
+		%[1]s%[1]s%[1]sgo
+		package main
+
+		import (
+			"fmt"
+		)
+
+		func main() {
+			fmt.Println("Hello, world!")
+		}
+		%[1]s%[1]s%[1]s
+	`, "`")
+
+	shellCodeBlock := heredoc.Docf(`
+		%[1]s%[1]s%[1]sshell
+		# list all repositories for a user
+		$ gh api graphql --paginate -f query='
+			query($endCursor: String) {
+				viewer {
+					repositories(first: 100, after: $endCursor) {
+						nodes { nameWithOwner }
+						pageInfo {
+							hasNextPage
+							endCursor
+						}
+					}
+				}
+			}
+		'
+		%[1]s%[1]s%[1]s
+	`)
+
+	tests := []struct {
+		name              string
+		text              string
+		theme             string
+		accessible        bool
+		wantColourModes   []ansi.ColourMode
+		allowDimFaintText bool
+	}{
+		{
+			name:              "when the light theme is selected, the Go codeblock renders using 8-bit colors",
+			text:              goCodeBlock,
+			theme:             "light",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the dark theme is selected, the Go codeblock renders using 8-bit colors",
+			text:              goCodeBlock,
+			theme:             "dark",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the accessible env var is set and the light theme is selected, the Go codeblock renders using 4-bit colors without dim/faint text",
+			text:              goCodeBlock,
+			theme:             "light",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		{
+			name:              "when the accessible env var is set and the dark theme is selected, the Go codeblock renders using 4-bit colors without dim/faint text",
+			text:              goCodeBlock,
+			theme:             "dark",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		{
+			name:              "when the light theme is selected, the Shell codeblock renders using 8-bit colors",
+			text:              shellCodeBlock,
+			theme:             "light",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the dark theme is selected, the Shell codeblock renders using 8-bit colors",
+			text:              shellCodeBlock,
+			theme:             "dark",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the accessible env var is set and the light theme is selected, the Shell codeblock renders using 4-bit colors without dim/faint text",
+			text:              shellCodeBlock,
+			theme:             "light",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		{
+			name:              "when the accessible env var is set and the dark theme is selected, the Shell codeblock renders using 4-bit colors without dim/faint text",
+			text:              shellCodeBlock,
+			theme:             "dark",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.accessible {
+				t.Setenv(color.AccessibleColorsEnv, "true")
+			}
+
+			out, err := Render(tt.text, WithTheme(tt.theme))
+			require.NoError(t, err)
+
+			// Parse and test ANSI escape sequences from rendered markdown for inaccessible display attributes
+			styledText, err := ansi.Parse(out)
+			require.NoError(t, err)
+
+			for _, st := range styledText {
+				require.Containsf(t, tt.wantColourModes, st.ColourMode, "Unexpected color mode detected in '%s' at %d", st, st.Offset)
+
+				if st.Faint() {
+					require.Truef(t, tt.allowDimFaintText, "Unexpected dim/faint text detected in '%s' at %d", st, st.Offset)
+				}
+			}
+		})
+	}
+}
+
+// Test_RenderColor verifies that the proper ANSI color codes are applied to the rendered
+// markdown by examining the ANSI escape sequences in the output for the correct color
+// match. For more information on ANSI color codes, see
+// https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
+func Test_RenderColor(t *testing.T) {
 	t.Setenv("GLAMOUR_STYLE", "")
+
+	codeBlock := heredoc.Docf(`
+		%[1]s%[1]s%[1]sgo
+		fmt.Println("Hello, world!")
+		%[1]s%[1]s%[1]s
+	`, "`")
+
 	tests := []struct {
-		name  string
-		text  string
-		theme string
+		name             string
+		text             string
+		theme            string
+		styleEnvVar      string
+		accessibleEnvVar string
+		wantOut          string
 	}{
 		{
-			name:  "light style",
-			text:  "some text",
-			theme: "light",
+			name:    "when the light theme is selected, the h2 renders using the 8-bit blue 27 provided by glamour",
+			text:    "## h2",
+			theme:   "light",
+			wantOut: fmt.Sprintf("%s1mh2", glamourLightH2_8bitColorSeq),
+		},
+		{
+			name:    "when the dark theme is selected, the h2 renders using the 8-bit blue 39 provided by glamour",
+			text:    "## h2",
+			theme:   "dark",
+			wantOut: fmt.Sprintf("%s1mh2", glamourDarkH2_8bitColorSeq),
+		},
+		{
+			name:    "when no theme is selected, the h2 renders in plain text without ansi coloring",
+			text:    "## h2",
+			theme:   "none",
+			wantOut: "## h2",
+		},
+		{
+			name:        "when the style env var is set, we override the theme with that style",
+			text:        "## h2",
+			theme:       "light",
+			styleEnvVar: "customStyle",
+			wantOut:     fmt.Sprintf("%s1mh2", customH2_8bitColorSeq),
+		},
+		{
+			name:             "when the accessible env var is set and the light theme is selected, the h2 renders using the 4-bit magenta provided by the light accessible style",
+			text:             "## h2",
+			theme:            "light",
+			accessibleEnvVar: "true",
+			wantOut:          fmt.Sprintf("%s1mh2", magenta_4bitColorSeq),
+		},
+		{
+			name:             "when the accessible env var is set and the dark theme is selected, the h2 renders using the 4-bit bright magenta provided by the dark accessible style",
+			text:             "## h2",
+			theme:            "dark",
+			accessibleEnvVar: "true",
+			wantOut:          fmt.Sprintf("%s1mh2", brightMagenta_4bitColorSeq),
+		},
+		{
+			name:    "when the light theme is selected, the codeblock renders using 8-bit colors",
+			text:    codeBlock,
+			theme:   "light",
+			wantOut: "\x1b[0m\x1b[38;5;235mfmt\x1b[0m\x1b[38;5;210m.\x1b[0m\x1b[38;5;35mPrintln\x1b[0m\x1b[38;5;210m(\x1b[0m\x1b[38;5;95m\"Hello, world!\"\x1b[0m\x1b[38;5;210m)\x1b[0m",
 		},
 		{
-			name:  "dark style",
-			text:  "some text",
-			theme: "dark",
+			name:    "when the dark theme is selected, the codeblock renders using 8-bit colors",
+			text:    codeBlock,
+			theme:   "dark",
+			wantOut: "\x1b[0m\x1b[38;5;251mfmt\x1b[0m\x1b[38;5;187m.\x1b[0m\x1b[38;5;42mPrintln\x1b[0m\x1b[38;5;187m(\x1b[0m\x1b[38;5;173m\"Hello, world!\"\x1b[0m\x1b[38;5;187m)\x1b[0m",
 		},
 		{
-			name:  "notty style",
-			text:  "some text",
-			theme: "none",
+			name:             "when the accessible env var is set and the light theme is selected, the codeblock renders using 4-bit colors",
+			text:             codeBlock,
+			theme:            "light",
+			accessibleEnvVar: "true",
+			wantOut:          "\x1b[0m\x1b[30mfmt\x1b[0m\x1b[33m.\x1b[0m\x1b[36mPrintln\x1b[0m\x1b[33m(\x1b[0m\x1b[90m\"Hello, world!\"\x1b[0m\x1b[33m)\x1b[0m",
+		},
+		{
+			name:             "when the accessible env var is set and the dark theme is selected, the codeblock renders using 4-bit colors",
+			text:             codeBlock,
+			theme:            "dark",
+			accessibleEnvVar: "true",
+			wantOut:          "\x1b[0m\x1b[37mfmt\x1b[0m\x1b[37m.\x1b[0m\x1b[36mPrintln\x1b[0m\x1b[37m(\x1b[0m\x1b[33m\"Hello, world!\"\x1b[0m\x1b[37m)\x1b[0m",
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			_, err := Render(tt.text, WithTheme(tt.theme))
-			assert.NoError(t, err)
+			t.Cleanup(func() {
+				// Chroma caches charm style used to render codeblocks, it must be unregistered to avoid previously used style being reused.
+				delete(styles.Registry, "charm")
+			})
+			t.Setenv(color.AccessibleColorsEnv, tt.accessibleEnvVar)
+
+			if tt.styleEnvVar != "" {
+				path := filepath.Join(t.TempDir(), fmt.Sprintf("%s.json", tt.styleEnvVar))
+				err := os.WriteFile(path, []byte(customGlamourStyle(t)), 0644)
+				if err != nil {
+					t.Fatal(err)
path: pkg/markdown/markdown_test.go
user: login: williammartin
body: **Question**

Is there a reason we're not using `require.NoError(err)` here? Suspect not.
created_at: 2025-03-11T16:05:02Z
author_association: MEMBER
diff_hunk: @@ -7,20 +7,21 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - name: Set up Go
-        uses: actions/setup-go@v3
-        with:
-          go-version: "1.21"
-
       - name: Checkout repository
         uses: actions/checkout@v4
 
+      - name: Set up Go
+        uses: actions/setup-go@v5
+        with:
+          go-version-file: go.mod
+
       - name: Check dependencies
         run: |
           go mod tidy
           git diff --exit-code go.mod
 
       - name: Lint
-        uses: golangci/golangci-lint-action@v3.4.0
+        uses: golangci/golangci-lint-action@v6
         with:
-          version: v1.54
+          version: v1.64
+          problem-matchers: true
path: .github/workflows/lint.yml
user: login: andyfeller
body: Additional documentation on problem matchers:
https://github.com/actions/toolkit/blob/7ae578ddd11b9e33db1daaad2ffb39e1ffeada3f/docs/problem-matchers.md
created_at: 2025-03-11T16:25:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,92 @@
+package color
+
+import (
+	"testing"
+
+	"github.com/MakeNowJust/heredoc"
+	"github.com/cli/go-gh/v2/internal/testutils"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIsAccessibleColorsEnabled(t *testing.T) {
+	tests := []struct {
+		name        string
+		envVarValue string
+		cfgStr      string
+		wantOut     bool
+	}{
+		{
+			name:        "When the accessibility configuration and env var are both unset, it should return false",
+			envVarValue: "",
+			cfgStr:      "",
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is unset but the env var is set to something truthy (not '0' or 'false'), it should return true",
+			envVarValue: "1",
+			cfgStr:      "",
+			wantOut:     true,
+		},
+		{
+			name:        "When the accessibility configuration is unset and the env var returns '0', it should return false",
+			envVarValue: "0",
+			cfgStr:      "",
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is unset and the env var returns 'false', it should return false",
+			envVarValue: "false",
+			cfgStr:      "",
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is set to enabled and the env var is unset, it should return true",
+			envVarValue: "",
+			cfgStr:      accessibilityEnabledConfig(),
+			wantOut:     true,
+		},
+		{
+			name:        "When the accessibility configuration is set to disabled and the env var is unset, it should return false",
+			envVarValue: "",
+			cfgStr:      accessibilityDisabledConfig(),
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is set to disabled and the env var is set to something truthy (not '0' or 'false'), it should return true",
+			envVarValue: "true",
+			cfgStr:      accessibilityDisabledConfig(),
+			wantOut:     true,
+		},
+		{
+			name:        "When the accessibility configuration is set to enabled and the env var is set to '0', it should return false",
+			envVarValue: "0",
+			cfgStr:      accessibilityEnabledConfig(),
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is set to enabled and the env var is set to 'false', it should return false",
+			envVarValue: "false",
+			cfgStr:      accessibilityEnabledConfig(),
+			wantOut:     false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Setenv("GH_ACCESSIBLE_COLORS", tt.envVarValue)
+			testutils.StubConfig(t, tt.cfgStr)
+			assert.Equal(t, tt.wantOut, IsAccessibleColorsEnabled())
+		})
+	}
+}
+
+func accessibilityEnabledConfig() string {
+	return heredoc.Docf(`
+		%s: enabled
+	`, AccessibleColorsSetting)
path: pkg/x/color/accessibility_test.go
user: login: andyfeller
body: Makes sense!
created_at: 2025-03-11T20:38:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,92 @@
+package color
+
+import (
+	"testing"
+
+	"github.com/MakeNowJust/heredoc"
+	"github.com/cli/go-gh/v2/internal/testutils"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIsAccessibleColorsEnabled(t *testing.T) {
+	tests := []struct {
+		name        string
+		envVarValue string
+		cfgStr      string
+		wantOut     bool
+	}{
+		{
+			name:        "When the accessibility configuration and env var are both unset, it should return false",
+			envVarValue: "",
+			cfgStr:      "",
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is unset but the env var is set to something truthy (not '0' or 'false'), it should return true",
+			envVarValue: "1",
+			cfgStr:      "",
+			wantOut:     true,
+		},
+		{
+			name:        "When the accessibility configuration is unset and the env var returns '0', it should return false",
+			envVarValue: "0",
+			cfgStr:      "",
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is unset and the env var returns 'false', it should return false",
+			envVarValue: "false",
+			cfgStr:      "",
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is set to enabled and the env var is unset, it should return true",
+			envVarValue: "",
+			cfgStr:      accessibilityEnabledConfig(),
+			wantOut:     true,
+		},
+		{
+			name:        "When the accessibility configuration is set to disabled and the env var is unset, it should return false",
+			envVarValue: "",
+			cfgStr:      accessibilityDisabledConfig(),
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is set to disabled and the env var is set to something truthy (not '0' or 'false'), it should return true",
+			envVarValue: "true",
+			cfgStr:      accessibilityDisabledConfig(),
+			wantOut:     true,
+		},
+		{
+			name:        "When the accessibility configuration is set to enabled and the env var is set to '0', it should return false",
+			envVarValue: "0",
+			cfgStr:      accessibilityEnabledConfig(),
+			wantOut:     false,
+		},
+		{
+			name:        "When the accessibility configuration is set to enabled and the env var is set to 'false', it should return false",
+			envVarValue: "false",
+			cfgStr:      accessibilityEnabledConfig(),
+			wantOut:     false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Setenv("GH_ACCESSIBLE_COLORS", tt.envVarValue)
path: pkg/x/color/accessibility_test.go
user: login: andyfeller
body: Ooh good catch!
created_at: 2025-03-11T20:39:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,118 @@
+package markdown
+
+import (
+	"testing"
+
+	"github.com/charmbracelet/glamour/ansi"
+	"github.com/charmbracelet/glamour/styles"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestANSIColorCodeString(t *testing.T) {
path: pkg/markdown/accessibility_test.go
user: login: andyfeller
body: There isn't a comment in https://github.com/cli/go-gh/pull/182 but I recall at the time bringing up some past experiences relying upon enum and iota in other codebases.  Now this has been brought up completely out of the blue, I'm more confident maybe this changes.
created_at: 2025-03-11T20:42:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,38 +1,282 @@
 package markdown
 
 import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
 	"testing"
 
+	"github.com/MakeNowJust/heredoc"
+	"github.com/alecthomas/chroma/v2/styles"
+	"github.com/cli/go-gh/v2/pkg/x/color"
+	ansi "github.com/leaanthony/go-ansi-parser"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
-func Test_Render(t *testing.T) {
+// glamour theme colors found at https://github.com/charmbracelet/glamour/tree/master/styles
+const (
+	glamourLightH2_8bitColorSeq = "\x1b[38;5;27;"
+	glamourDarkH2_8bitColorSeq  = "\x1b[38;5;39;"
+	customH2_8bitColorSeq       = "\x1b[38;5;61;"
+	magenta_4bitColorSeq        = "\x1b[35;"
+	brightMagenta_4bitColorSeq  = "\x1b[95;"
+)
+
+// Test_RenderAccessible tests rendered markdown for accessibility concerns such as color mode / depth and other display attributes.
+// It works by parsing the rendered markdown for ANSI escape sequences and checking their display attributes.
+// Test scenarios allow multiple color mode / depths because `ansi.Parse()` considers `\x1b[0m` sequence as part of `ansi.Default`.
+func Test_RenderAccessible(t *testing.T) {
+	goCodeBlock := heredoc.Docf(`
+		%[1]s%[1]s%[1]sgo
+		package main
+
+		import (
+			"fmt"
+		)
+
+		func main() {
+			fmt.Println("Hello, world!")
+		}
+		%[1]s%[1]s%[1]s
+	`, "`")
+
+	shellCodeBlock := heredoc.Docf(`
+		%[1]s%[1]s%[1]sshell
+		# list all repositories for a user
+		$ gh api graphql --paginate -f query='
+			query($endCursor: String) {
+				viewer {
+					repositories(first: 100, after: $endCursor) {
+						nodes { nameWithOwner }
+						pageInfo {
+							hasNextPage
+							endCursor
+						}
+					}
+				}
+			}
+		'
+		%[1]s%[1]s%[1]s
+	`)
+
+	tests := []struct {
+		name              string
+		text              string
+		theme             string
+		accessible        bool
+		wantColourModes   []ansi.ColourMode
+		allowDimFaintText bool
+	}{
+		{
+			name:              "when the light theme is selected, the Go codeblock renders using 8-bit colors",
+			text:              goCodeBlock,
+			theme:             "light",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the dark theme is selected, the Go codeblock renders using 8-bit colors",
+			text:              goCodeBlock,
+			theme:             "dark",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the accessible env var is set and the light theme is selected, the Go codeblock renders using 4-bit colors without dim/faint text",
+			text:              goCodeBlock,
+			theme:             "light",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		{
+			name:              "when the accessible env var is set and the dark theme is selected, the Go codeblock renders using 4-bit colors without dim/faint text",
+			text:              goCodeBlock,
+			theme:             "dark",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		{
+			name:              "when the light theme is selected, the Shell codeblock renders using 8-bit colors",
+			text:              shellCodeBlock,
+			theme:             "light",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the dark theme is selected, the Shell codeblock renders using 8-bit colors",
+			text:              shellCodeBlock,
+			theme:             "dark",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the accessible env var is set and the light theme is selected, the Shell codeblock renders using 4-bit colors without dim/faint text",
+			text:              shellCodeBlock,
+			theme:             "light",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		{
+			name:              "when the accessible env var is set and the dark theme is selected, the Shell codeblock renders using 4-bit colors without dim/faint text",
+			text:              shellCodeBlock,
+			theme:             "dark",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.accessible {
+				t.Setenv(color.AccessibleColorsEnv, "true")
+			}
+
+			out, err := Render(tt.text, WithTheme(tt.theme))
+			require.NoError(t, err)
+
+			// Parse and test ANSI escape sequences from rendered markdown for inaccessible display attributes
+			styledText, err := ansi.Parse(out)
+			require.NoError(t, err)
+
+			for _, st := range styledText {
+				require.Containsf(t, tt.wantColourModes, st.ColourMode, "Unexpected color mode detected in '%s' at %d", st, st.Offset)
+
+				if st.Faint() {
+					require.Truef(t, tt.allowDimFaintText, "Unexpected dim/faint text detected in '%s' at %d", st, st.Offset)
+				}
+			}
+		})
+	}
+}
+
+// Test_RenderColor verifies that the proper ANSI color codes are applied to the rendered
+// markdown by examining the ANSI escape sequences in the output for the correct color
+// match. For more information on ANSI color codes, see
+// https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
+func Test_RenderColor(t *testing.T) {
 	t.Setenv("GLAMOUR_STYLE", "")
+
+	codeBlock := heredoc.Docf(`
+		%[1]s%[1]s%[1]sgo
+		fmt.Println("Hello, world!")
+		%[1]s%[1]s%[1]s
+	`, "`")
+
 	tests := []struct {
-		name  string
-		text  string
-		theme string
+		name             string
+		text             string
+		theme            string
+		styleEnvVar      string
+		accessibleEnvVar string
+		wantOut          string
 	}{
 		{
-			name:  "light style",
-			text:  "some text",
-			theme: "light",
+			name:    "when the light theme is selected, the h2 renders using the 8-bit blue 27 provided by glamour",
+			text:    "## h2",
+			theme:   "light",
+			wantOut: fmt.Sprintf("%s1mh2", glamourLightH2_8bitColorSeq),
+		},
+		{
+			name:    "when the dark theme is selected, the h2 renders using the 8-bit blue 39 provided by glamour",
+			text:    "## h2",
+			theme:   "dark",
+			wantOut: fmt.Sprintf("%s1mh2", glamourDarkH2_8bitColorSeq),
+		},
+		{
+			name:    "when no theme is selected, the h2 renders in plain text without ansi coloring",
+			text:    "## h2",
+			theme:   "none",
+			wantOut: "## h2",
+		},
+		{
+			name:        "when the style env var is set, we override the theme with that style",
+			text:        "## h2",
+			theme:       "light",
+			styleEnvVar: "customStyle",
+			wantOut:     fmt.Sprintf("%s1mh2", customH2_8bitColorSeq),
+		},
+		{
+			name:             "when the accessible env var is set and the light theme is selected, the h2 renders using the 4-bit magenta provided by the light accessible style",
+			text:             "## h2",
+			theme:            "light",
+			accessibleEnvVar: "true",
+			wantOut:          fmt.Sprintf("%s1mh2", magenta_4bitColorSeq),
+		},
+		{
+			name:             "when the accessible env var is set and the dark theme is selected, the h2 renders using the 4-bit bright magenta provided by the dark accessible style",
+			text:             "## h2",
+			theme:            "dark",
+			accessibleEnvVar: "true",
+			wantOut:          fmt.Sprintf("%s1mh2", brightMagenta_4bitColorSeq),
+		},
+		{
+			name:    "when the light theme is selected, the codeblock renders using 8-bit colors",
+			text:    codeBlock,
+			theme:   "light",
+			wantOut: "\x1b[0m\x1b[38;5;235mfmt\x1b[0m\x1b[38;5;210m.\x1b[0m\x1b[38;5;35mPrintln\x1b[0m\x1b[38;5;210m(\x1b[0m\x1b[38;5;95m\"Hello, world!\"\x1b[0m\x1b[38;5;210m)\x1b[0m",
 		},
 		{
-			name:  "dark style",
-			text:  "some text",
-			theme: "dark",
+			name:    "when the dark theme is selected, the codeblock renders using 8-bit colors",
+			text:    codeBlock,
+			theme:   "dark",
+			wantOut: "\x1b[0m\x1b[38;5;251mfmt\x1b[0m\x1b[38;5;187m.\x1b[0m\x1b[38;5;42mPrintln\x1b[0m\x1b[38;5;187m(\x1b[0m\x1b[38;5;173m\"Hello, world!\"\x1b[0m\x1b[38;5;187m)\x1b[0m",
 		},
 		{
-			name:  "notty style",
-			text:  "some text",
-			theme: "none",
+			name:             "when the accessible env var is set and the light theme is selected, the codeblock renders using 4-bit colors",
+			text:             codeBlock,
+			theme:            "light",
+			accessibleEnvVar: "true",
+			wantOut:          "\x1b[0m\x1b[30mfmt\x1b[0m\x1b[33m.\x1b[0m\x1b[36mPrintln\x1b[0m\x1b[33m(\x1b[0m\x1b[90m\"Hello, world!\"\x1b[0m\x1b[33m)\x1b[0m",
+		},
+		{
+			name:             "when the accessible env var is set and the dark theme is selected, the codeblock renders using 4-bit colors",
+			text:             codeBlock,
+			theme:            "dark",
+			accessibleEnvVar: "true",
+			wantOut:          "\x1b[0m\x1b[37mfmt\x1b[0m\x1b[37m.\x1b[0m\x1b[36mPrintln\x1b[0m\x1b[37m(\x1b[0m\x1b[33m\"Hello, world!\"\x1b[0m\x1b[37m)\x1b[0m",
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			_, err := Render(tt.text, WithTheme(tt.theme))
-			assert.NoError(t, err)
+			t.Cleanup(func() {
+				// Chroma caches charm style used to render codeblocks, it must be unregistered to avoid previously used style being reused.
+				delete(styles.Registry, "charm")
+			})
+			t.Setenv(color.AccessibleColorsEnv, tt.accessibleEnvVar)
+
+			if tt.styleEnvVar != "" {
+				path := filepath.Join(t.TempDir(), fmt.Sprintf("%s.json", tt.styleEnvVar))
+				err := os.WriteFile(path, []byte(customGlamourStyle(t)), 0644)
+				if err != nil {
+					t.Fatal(err)
path: pkg/markdown/markdown_test.go
user: login: andyfeller
body: I'm unsure and comfortable changing it.

What are situations you would expect `t.Fatal(...)` being used?
created_at: 2025-03-11T20:46:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,38 +1,282 @@
 package markdown
 
 import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
 	"testing"
 
+	"github.com/MakeNowJust/heredoc"
+	"github.com/alecthomas/chroma/v2/styles"
+	"github.com/cli/go-gh/v2/pkg/x/color"
+	ansi "github.com/leaanthony/go-ansi-parser"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
-func Test_Render(t *testing.T) {
+// glamour theme colors found at https://github.com/charmbracelet/glamour/tree/master/styles
+const (
+	glamourLightH2_8bitColorSeq = "\x1b[38;5;27;"
+	glamourDarkH2_8bitColorSeq  = "\x1b[38;5;39;"
+	customH2_8bitColorSeq       = "\x1b[38;5;61;"
+	magenta_4bitColorSeq        = "\x1b[35;"
+	brightMagenta_4bitColorSeq  = "\x1b[95;"
+)
+
+// Test_RenderAccessible tests rendered markdown for accessibility concerns such as color mode / depth and other display attributes.
+// It works by parsing the rendered markdown for ANSI escape sequences and checking their display attributes.
+// Test scenarios allow multiple color mode / depths because `ansi.Parse()` considers `\x1b[0m` sequence as part of `ansi.Default`.
+func Test_RenderAccessible(t *testing.T) {
+	goCodeBlock := heredoc.Docf(`
+		%[1]s%[1]s%[1]sgo
+		package main
+
+		import (
+			"fmt"
+		)
+
+		func main() {
+			fmt.Println("Hello, world!")
+		}
+		%[1]s%[1]s%[1]s
+	`, "`")
+
+	shellCodeBlock := heredoc.Docf(`
+		%[1]s%[1]s%[1]sshell
+		# list all repositories for a user
+		$ gh api graphql --paginate -f query='
+			query($endCursor: String) {
+				viewer {
+					repositories(first: 100, after: $endCursor) {
+						nodes { nameWithOwner }
+						pageInfo {
+							hasNextPage
+							endCursor
+						}
+					}
+				}
+			}
+		'
+		%[1]s%[1]s%[1]s
+	`)
+
+	tests := []struct {
+		name              string
+		text              string
+		theme             string
+		accessible        bool
+		wantColourModes   []ansi.ColourMode
+		allowDimFaintText bool
+	}{
+		{
+			name:              "when the light theme is selected, the Go codeblock renders using 8-bit colors",
+			text:              goCodeBlock,
+			theme:             "light",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the dark theme is selected, the Go codeblock renders using 8-bit colors",
+			text:              goCodeBlock,
+			theme:             "dark",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the accessible env var is set and the light theme is selected, the Go codeblock renders using 4-bit colors without dim/faint text",
+			text:              goCodeBlock,
+			theme:             "light",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		{
+			name:              "when the accessible env var is set and the dark theme is selected, the Go codeblock renders using 4-bit colors without dim/faint text",
+			text:              goCodeBlock,
+			theme:             "dark",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		{
+			name:              "when the light theme is selected, the Shell codeblock renders using 8-bit colors",
+			text:              shellCodeBlock,
+			theme:             "light",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the dark theme is selected, the Shell codeblock renders using 8-bit colors",
+			text:              shellCodeBlock,
+			theme:             "dark",
+			wantColourModes:   []ansi.ColourMode{ansi.Default, ansi.TwoFiveSix},
+			allowDimFaintText: true,
+		},
+		{
+			name:              "when the accessible env var is set and the light theme is selected, the Shell codeblock renders using 4-bit colors without dim/faint text",
+			text:              shellCodeBlock,
+			theme:             "light",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+		{
+			name:              "when the accessible env var is set and the dark theme is selected, the Shell codeblock renders using 4-bit colors without dim/faint text",
+			text:              shellCodeBlock,
+			theme:             "dark",
+			accessible:        true,
+			wantColourModes:   []ansi.ColourMode{ansi.Default},
+			allowDimFaintText: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.accessible {
+				t.Setenv(color.AccessibleColorsEnv, "true")
+			}
+
+			out, err := Render(tt.text, WithTheme(tt.theme))
+			require.NoError(t, err)
+
+			// Parse and test ANSI escape sequences from rendered markdown for inaccessible display attributes
+			styledText, err := ansi.Parse(out)
+			require.NoError(t, err)
+
+			for _, st := range styledText {
+				require.Containsf(t, tt.wantColourModes, st.ColourMode, "Unexpected color mode detected in '%s' at %d", st, st.Offset)
+
+				if st.Faint() {
+					require.Truef(t, tt.allowDimFaintText, "Unexpected dim/faint text detected in '%s' at %d", st, st.Offset)
+				}
+			}
+		})
+	}
+}
+
+// Test_RenderColor verifies that the proper ANSI color codes are applied to the rendered
+// markdown by examining the ANSI escape sequences in the output for the correct color
+// match. For more information on ANSI color codes, see
+// https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
+func Test_RenderColor(t *testing.T) {
 	t.Setenv("GLAMOUR_STYLE", "")
+
+	codeBlock := heredoc.Docf(`
+		%[1]s%[1]s%[1]sgo
+		fmt.Println("Hello, world!")
+		%[1]s%[1]s%[1]s
+	`, "`")
+
 	tests := []struct {
-		name  string
-		text  string
-		theme string
+		name             string
+		text             string
+		theme            string
+		styleEnvVar      string
+		accessibleEnvVar string
+		wantOut          string
 	}{
 		{
-			name:  "light style",
-			text:  "some text",
-			theme: "light",
+			name:    "when the light theme is selected, the h2 renders using the 8-bit blue 27 provided by glamour",
+			text:    "## h2",
+			theme:   "light",
+			wantOut: fmt.Sprintf("%s1mh2", glamourLightH2_8bitColorSeq),
+		},
+		{
+			name:    "when the dark theme is selected, the h2 renders using the 8-bit blue 39 provided by glamour",
+			text:    "## h2",
+			theme:   "dark",
+			wantOut: fmt.Sprintf("%s1mh2", glamourDarkH2_8bitColorSeq),
+		},
+		{
+			name:    "when no theme is selected, the h2 renders in plain text without ansi coloring",
+			text:    "## h2",
+			theme:   "none",
+			wantOut: "## h2",
+		},
+		{
+			name:        "when the style env var is set, we override the theme with that style",
+			text:        "## h2",
+			theme:       "light",
+			styleEnvVar: "customStyle",
+			wantOut:     fmt.Sprintf("%s1mh2", customH2_8bitColorSeq),
+		},
+		{
+			name:             "when the accessible env var is set and the light theme is selected, the h2 renders using the 4-bit magenta provided by the light accessible style",
+			text:             "## h2",
+			theme:            "light",
+			accessibleEnvVar: "true",
+			wantOut:          fmt.Sprintf("%s1mh2", magenta_4bitColorSeq),
+		},
+		{
+			name:             "when the accessible env var is set and the dark theme is selected, the h2 renders using the 4-bit bright magenta provided by the dark accessible style",
+			text:             "## h2",
+			theme:            "dark",
+			accessibleEnvVar: "true",
+			wantOut:          fmt.Sprintf("%s1mh2", brightMagenta_4bitColorSeq),
+		},
+		{
+			name:    "when the light theme is selected, the codeblock renders using 8-bit colors",
+			text:    codeBlock,
+			theme:   "light",
+			wantOut: "\x1b[0m\x1b[38;5;235mfmt\x1b[0m\x1b[38;5;210m.\x1b[0m\x1b[38;5;35mPrintln\x1b[0m\x1b[38;5;210m(\x1b[0m\x1b[38;5;95m\"Hello, world!\"\x1b[0m\x1b[38;5;210m)\x1b[0m",
 		},
 		{
-			name:  "dark style",
-			text:  "some text",
-			theme: "dark",
+			name:    "when the dark theme is selected, the codeblock renders using 8-bit colors",
+			text:    codeBlock,
+			theme:   "dark",
+			wantOut: "\x1b[0m\x1b[38;5;251mfmt\x1b[0m\x1b[38;5;187m.\x1b[0m\x1b[38;5;42mPrintln\x1b[0m\x1b[38;5;187m(\x1b[0m\x1b[38;5;173m\"Hello, world!\"\x1b[0m\x1b[38;5;187m)\x1b[0m",
 		},
 		{
-			name:  "notty style",
-			text:  "some text",
-			theme: "none",
+			name:             "when the accessible env var is set and the light theme is selected, the codeblock renders using 4-bit colors",
+			text:             codeBlock,
+			theme:            "light",
+			accessibleEnvVar: "true",
+			wantOut:          "\x1b[0m\x1b[30mfmt\x1b[0m\x1b[33m.\x1b[0m\x1b[36mPrintln\x1b[0m\x1b[33m(\x1b[0m\x1b[90m\"Hello, world!\"\x1b[0m\x1b[33m)\x1b[0m",
+		},
+		{
+			name:             "when the accessible env var is set and the dark theme is selected, the codeblock renders using 4-bit colors",
+			text:             codeBlock,
+			theme:            "dark",
+			accessibleEnvVar: "true",
+			wantOut:          "\x1b[0m\x1b[37mfmt\x1b[0m\x1b[37m.\x1b[0m\x1b[36mPrintln\x1b[0m\x1b[37m(\x1b[0m\x1b[33m\"Hello, world!\"\x1b[0m\x1b[37m)\x1b[0m",
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			_, err := Render(tt.text, WithTheme(tt.theme))
-			assert.NoError(t, err)
+			t.Cleanup(func() {
+				// Chroma caches charm style used to render codeblocks, it must be unregistered to avoid previously used style being reused.
+				delete(styles.Registry, "charm")
+			})
+			t.Setenv(color.AccessibleColorsEnv, tt.accessibleEnvVar)
+
+			if tt.styleEnvVar != "" {
+				path := filepath.Join(t.TempDir(), fmt.Sprintf("%s.json", tt.styleEnvVar))
+				err := os.WriteFile(path, []byte(customGlamourStyle(t)), 0644)
+				if err != nil {
+					t.Fatal(err)
path: pkg/markdown/markdown_test.go
user: login: williammartin
body: None, if you're using `testify` across the codebase. If you're not using an assertion library, then I would expect to see it.
created_at: 2025-03-12T10:10:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,21 @@
+package testutils
+
+import (
+	"testing"
+
+	"github.com/cli/go-gh/v2/pkg/config"
+)
+
+// StubConfig replaces the config.Read function with a function that returns a config object
+// created from the given config string. It also sets up a cleanup function that restores the
+// original config.Read function.
+func StubConfig(t *testing.T, cfgStr string) {
path: internal/testutils/config_stub.go
user: login: andyfeller
body: I'm down for this, but would probably like to sync over a weird behavior around importing the package I'm seeing while acting on this. 🤔
created_at: 2025-03-12T12:24:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,106 @@
+package markdown
+
+import (
+	"strconv"
+
+	"github.com/charmbracelet/glamour/ansi"
+	"github.com/charmbracelet/glamour/styles"
+)
+
+type ANSIColorCode int
+
+const (
+	Black ANSIColorCode = iota
+	Red
+	Green
+	Yellow
+	Blue
+	Magenta
+	Cyan
+	White
+	BrightBlack
+	BrightRed
+	BrightGreen
+	BrightYellow
+	BrightBlue
+	BrightMagenta
+	BrightCyan
+	BrightWhite
+)
+
+func (a ANSIColorCode) String() string {
+	return strconv.Itoa(int(a))
+}
+
+func (a ANSIColorCode) StrPtr() *string {
path: pkg/markdown/accessibility.go
user: login: andyfeller
body: If we're moving this under `x`, don't we have to export it?
created_at: 2025-03-12T12:26:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,21 @@
+package testutils
+
+import (
+	"testing"
+
+	"github.com/cli/go-gh/v2/pkg/config"
+)
+
+// StubConfig replaces the config.Read function with a function that returns a config object
+// created from the given config string. It also sets up a cleanup function that restores the
+// original config.Read function.
+func StubConfig(t *testing.T, cfgStr string) {
path: internal/testutils/config_stub.go
user: login: williammartin
body: On second thoughts it might not be possible to import from a different package for `_test` tags. Sorry for the wild goose chase.
created_at: 2025-03-12T13:07:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,106 @@
+package markdown
+
+import (
+	"strconv"
+
+	"github.com/charmbracelet/glamour/ansi"
+	"github.com/charmbracelet/glamour/styles"
+)
+
+type ANSIColorCode int
+
+const (
+	Black ANSIColorCode = iota
+	Red
+	Green
+	Yellow
+	Blue
+	Magenta
+	Cyan
+	White
+	BrightBlack
+	BrightRed
+	BrightGreen
+	BrightYellow
+	BrightBlue
+	BrightMagenta
+	BrightCyan
+	BrightWhite
+)
+
+func (a ANSIColorCode) String() string {
+	return strconv.Itoa(int(a))
+}
+
+func (a ANSIColorCode) StrPtr() *string {
path: pkg/markdown/accessibility.go
user: login: williammartin
body: It kind of looked like `StrPtr` was only used in this package. Am I missing something?
created_at: 2025-03-12T13:09:52Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,106 @@
+package markdown
path: pkg/markdown/accessibility.go
user: login: andyfeller
body: Related question: any preference on the alias used when importing `x` packages?

I know there are broader Go conventions on the topic, but I don't think I've encountered another project that ran into this situation to have a strong opinion and thus went with:

```go
// Package markdown facilitates rendering markdown in the terminal.
package markdown

import (
	"os"
	"strings"

	"github.com/charmbracelet/glamour"
	xcolor "github.com/cli/go-gh/v2/pkg/x/color"
	xmarkdown "github.com/cli/go-gh/v2/pkg/x/markdown"
)
```
created_at: 2025-03-12T14:04:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,106 @@
+package markdown
path: pkg/markdown/accessibility.go
user: login: williammartin
body: No opinions.
created_at: 2025-03-12T14:05:51Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,106 @@
+package markdown
+
+import (
+	"strconv"
+
+	"github.com/charmbracelet/glamour/ansi"
+	"github.com/charmbracelet/glamour/styles"
+)
+
+type ANSIColorCode int
+
+const (
+	Black ANSIColorCode = iota
+	Red
+	Green
+	Yellow
+	Blue
+	Magenta
+	Cyan
+	White
+	BrightBlack
+	BrightRed
+	BrightGreen
+	BrightYellow
+	BrightBlue
+	BrightMagenta
+	BrightCyan
+	BrightWhite
+)
+
+func (a ANSIColorCode) String() string {
+	return strconv.Itoa(int(a))
+}
+
+func (a ANSIColorCode) StrPtr() *string {
path: pkg/markdown/accessibility.go
user: login: andyfeller
body: No you aren't 🚀 let's unexport it.
created_at: 2025-03-12T22:33:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,118 @@
+package markdown
+
+import (
+	"testing"
+
+	"github.com/charmbracelet/glamour/ansi"
+	"github.com/charmbracelet/glamour/styles"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestANSIColorCodeString(t *testing.T) {
path: pkg/markdown/accessibility_test.go
user: login: andyfeller
body: So after a bunch of back and forth, here are my thoughts:

1. The enumeration is the only way we can really do compile time type checking, so will leave it for now
2. I think ANSI color code is a little misleading _(technically ANSI colors = 3-bit)_
3. I don't know if we need separate `String` and `StrPtr` functions
4. I think renaming this to explain its purpose might be easier for people to understand; basically these are glamour style color code constants
5. I agree we should test all the colors

I pushed some changes up and happy to talk through them.
created_at: 2025-03-12T22:36:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,165 @@
+package markdown
+
+import (
+	"testing"
+
+	"github.com/charmbracelet/glamour/ansi"
+	"github.com/charmbracelet/glamour/styles"
+	"github.com/stretchr/testify/assert"
+)
+
+// TestGlamourStyleColors ensures that the resulting string color codes match the expected values.
+func TestGlamourStyleColors(t *testing.T) {
+	tests := []struct {
+		name string
+		c    glamourStyleColor
+		want string
+	}{
+		{
+			name: "black",
+			c:    black,
+			want: "0",
+		},
+		{
+			name: "red",
+			c:    red,
+			want: "1",
+		},
+		{
+			name: "green",
+			c:    green,
+			want: "2",
+		},
+		{
+			name: "yellow",
+			c:    yellow,
+			want: "3",
+		},
+		{
+			name: "blue",
+			c:    blue,
+			want: "4",
+		},
+		{
+			name: "magenta",
+			c:    magenta,
+			want: "5",
+		},
+		{
+			name: "cyan",
+			c:    cyan,
+			want: "6",
+		},
+		{
+			name: "white",
+			c:    white,
+			want: "7",
+		},
+		{
+			name: "bright black",
+			c:    brightBlack,
+			want: "8",
+		},
+		{
+			name: "bright red",
+			c:    brightRed,
+			want: "9",
+		},
+		{
+			name: "bright green",
+			c:    brightGreen,
+			want: "10",
+		},
+		{
+			name: "bright yellow",
+			c:    brightYellow,
+			want: "11",
+		},
+		{
+			name: "bright blue",
+			c:    brightBlue,
+			want: "12",
+		},
+		{
+			name: "bright magenta",
+			c:    brightMagenta,
+			want: "13",
+		},
+		{
+			name: "bright cyan",
+			c:    brightCyan,
+			want: "14",
+		},
+		{
+			name: "bright white",
+			c:    brightWhite,
+			want: "15",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
path: pkg/x/markdown/accessibility_test.go
user: login: williammartin
body: **Suggestion**

Mark the subtests with `t.Parallel()` since there's nothing that prevents them being parallelised. Same for all the other tests.
created_at: 2025-03-13T10:46:07Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,165 @@
+package markdown
+
+import (
+	"testing"
+
+	"github.com/charmbracelet/glamour/ansi"
+	"github.com/charmbracelet/glamour/styles"
+	"github.com/stretchr/testify/assert"
+)
+
+// TestGlamourStyleColors ensures that the resulting string color codes match the expected values.
+func TestGlamourStyleColors(t *testing.T) {
+	tests := []struct {
+		name string
+		c    glamourStyleColor
+		want string
+	}{
+		{
+			name: "black",
+			c:    black,
+			want: "0",
+		},
+		{
+			name: "red",
+			c:    red,
+			want: "1",
+		},
+		{
+			name: "green",
+			c:    green,
+			want: "2",
+		},
+		{
+			name: "yellow",
+			c:    yellow,
+			want: "3",
+		},
+		{
+			name: "blue",
+			c:    blue,
+			want: "4",
+		},
+		{
+			name: "magenta",
+			c:    magenta,
+			want: "5",
+		},
+		{
+			name: "cyan",
+			c:    cyan,
+			want: "6",
+		},
+		{
+			name: "white",
+			c:    white,
+			want: "7",
+		},
+		{
+			name: "bright black",
+			c:    brightBlack,
+			want: "8",
+		},
+		{
+			name: "bright red",
+			c:    brightRed,
+			want: "9",
+		},
+		{
+			name: "bright green",
+			c:    brightGreen,
+			want: "10",
+		},
+		{
+			name: "bright yellow",
+			c:    brightYellow,
+			want: "11",
+		},
+		{
+			name: "bright blue",
+			c:    brightBlue,
+			want: "12",
+		},
+		{
+			name: "bright magenta",
+			c:    brightMagenta,
+			want: "13",
+		},
+		{
+			name: "bright cyan",
+			c:    brightCyan,
+			want: "14",
+		},
+		{
+			name: "bright white",
+			c:    brightWhite,
+			want: "15",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
path: pkg/x/markdown/accessibility_test.go
user: login: andyfeller
body: TIL `t.Parallel()` cannot be used with `t.Setenv()`

That said, I got as many as I could.
created_at: 2025-03-20T21:37:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,27 +1,31 @@
 module github.com/cli/go-gh/v2
 
-go 1.21
+go 1.23.0
+
+toolchain go1.23.7
path: go.mod
user: login: williammartin
body: Is this really required? Would think it is unusual to have a toolchain entry for a library.
created_at: 2025-03-25T10:31:20Z
author_association: MEMBER
diff_hunk: @@ -1,27 +1,31 @@
 module github.com/cli/go-gh/v2
 
-go 1.21
+go 1.23.0
+
+toolchain go1.23.7
path: go.mod
user: login: andyfeller
body: Great question; `go mod` did this in upgrading dependencies 🤦 
created_at: 2025-03-25T11:48:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,27 +1,31 @@
 module github.com/cli/go-gh/v2
 
-go 1.21
+go 1.23.0
+
+toolchain go1.23.7
path: go.mod
user: login: andyfeller
body: Removing for now
created_at: 2025-03-25T11:54:41Z
author_association: CONTRIBUTOR

PR comments:
user: login: andyfeller
created_at: 2025-03-10T14:44:11Z
body: In addressing the lint errors, I've made minor changes to GitHub Actions workflows to pull Go version from `go.mod`, which requires a newer version of `actions/setup-go`.

Separately while troubleshooting lint job, I notice that the action used for Go linting is 3 major version behind; [`v3.4.0`](https://github.com/golangci/golangci-lint-action/releases/tag/v3.4.0) => [`v6.5.0`](https://github.com/golangci/golangci-lint-action/releases/tag/v6.5.0).

For changes, see [diff](https://github.com/golangci/golangci-lint-action/compare/v3.4.0...v6.5.0)
author_association: CONTRIBUTOR
user: login: andyfeller
created_at: 2025-03-10T16:35:20Z
body: > [!NOTE]
> In upping the Go version, there is a discrepancy between automation in the default branch which takes 1.21 into account whereas this branch bumps it up. 🤷 
>
> Meaning there will be some follow up work to the repository ruleset / branch protection rules afterwards.
author_association: CONTRIBUTOR
user: login: williammartin
created_at: 2025-03-13T11:05:33Z
body: @andyfeller There is text in the images above "Image: screenshot of gh pr status → https://user-images.githubusercontent.com/98482/84171218-327e7a80-aa40-11ea-8cd1-5177fc2d0e72.png" and I'm having trouble convincing myself that it is using ANSI 4-bit colors.

<img width="785" alt="image" src="https://github.com/user-attachments/assets/198d3945-3e3d-45b4-8e7e-83db84e38443" />

From using `sequin` it looks like the control code is:

```
 CSI 37m: Foreground color: White
 CSI 38;5;243m: Foreground color: 243
```

So this looks like a 256-bit color code. I think this is the relevant style bits are:
 * https://github.com/charmbracelet/glamour/blob/d1d5125cfab42203495f15b846e901d0c1f37aaa/styles/styles.go#L233
 * https://github.com/charmbracelet/glamour/blob/d1d5125cfab42203495f15b846e901d0c1f37aaa/styles/styles.go#L442

---

It also looks to me as if [LinkText](https://github.com/charmbracelet/glamour/blob/d1d5125cfab42203495f15b846e901d0c1f37aaa/styles/styles.go#L434) would not be 16-bit, but I could be mistaken.

Here's the green `LinkText` "hub" on my dark background:

<img width="506" alt="image" src="https://github.com/user-attachments/assets/10c2787f-2fdc-4dab-b85d-b799d24995b4" />

Indeed, when I run it through `sequin` I see:

```
 CSI 0m: Reset style
 CSI 38;5;35;1m: Foreground color: 35, Bold
Text more detailed explanation
```

Which matches the `DarkStyleConfig` [LinkTest](https://github.com/charmbracelet/glamour/blob/d1d5125cfab42203495f15b846e901d0c1f37aaa/styles/styles.go#L224-L226)
author_association: MEMBER
user: login: andyfeller
created_at: 2025-03-13T19:46:01Z
body: ## Accessible colors should also be used when `GLAMOUR_STYLE` is `dark` or `light`

### Problem

When determining how to render markdown, the code is concerned with 2 things:

1. Is the terminal background `dark` or `light`?  Or should `none` colors be used? _(`theme`)_ 

2. How does the user want to stylize markdown? _(`style`)_
   - Using a built-in [`charmbracelet/glamour` style](https://github.com/cli/go-gh/blob/7a6e5543a7f13a372dfafa88fac99981496b4991/pkg/term/env.go#L139-L148) _(`ascii`, `auto`, `dark`, `dracula`, `tokyo-night`, `light`, `notty`, `pink`)_
   - Using a path to a [custom `charmbracelet/glamour` style](https://github.com/charmbracelet/glamour/tree/master?tab=readme-ov-file#styles) defined as JSON

Currently, **accessible colors are only applied if `style` has not been set**, which can be confusing if the user sets `GLAMOUR_STYLE` to `dark` or `light`:

https://github.com/cli/go-gh/blob/bde34d5cca49d621bce438a47b3ab94c4246856a/pkg/markdown/markdown.go#L33-L53

- `GH_ACCESSIBLE_COLORS=1 bin/gh repo view andyfeller/markdown-lorem-ipsum`

  As we see, `gh` is using the custom accessible themes when `GLAMOUR_STYLE` is unset:

  ![Screenshot of new accessible experience without glamour env variable](https://github.com/user-attachments/assets/edd743c6-0a72-4e9a-9fd6-67293c3cdee9)

- `GLAMOUR_STYLE=dark GH_ACCESSIBLE_COLORS=1 bin/gh repo view andyfeller/markdown-lorem-ipsum`

  However we see `gh` will fallback to the current markdown rendering experience even when the `style` and `theme` should be the same.

  ![Screenshot of new accessible experience with glamour env variable](https://github.com/user-attachments/assets/c4801c18-ea45-4043-807a-05eb52d4b4b5)

### Suggestion

This was honestly a bit confusing for me and I've been working on these changes for a while as I expected `GLAMOUR_STYLE=dark` or `GLAMOUR_STYLE=light` to also pick up accessible colors.

If this logic was enhanced to allow those 2 `styles` to fall into the same logic that applies accessible colors, I think we'll have fewer people surprised.
author_association: CONTRIBUTOR
user: login: andyfeller
created_at: 2025-03-13T20:12:17Z
body: ## Should tables be wrapped or not? 🤔 

One of the improvements in `charmbracelet/glamour@v0.9.0` is [greater control over table wrapping / overflow](https://github.com/charmbracelet/lipgloss/releases/tag/v1.1.0), which `glamour` now [does for tables by default](https://github.com/charmbracelet/glamour/blob/f43b1ad9ef09b10a93837e07ae2c18b66bceac5c/ansi/table.go#L35-L61):

```go
	ctx.table.lipgloss = table.New().Width(width).Wrap(true)
```

**UPDATE: This PR has been updated with upstream changes to disable table content wrapping until we can understand its impact to screenreader accessibility.**

<details>
<summary>
  <b>Expand for full thoughts</b>
</summary>
<p>

I'm going to enhance https://github.com/andyfeller/markdown-lorem-ipsum to contain an example for discussion as I'm unsure how this affects accessible screenreader experience, which is outside the scope of the issue and PR here.

### Current `gh` markdown table rendering experience

<img width="1128" alt="Screenshot 2025-03-13 at 5 54 04 PM" src="https://github.com/user-attachments/assets/492dc5b5-3caa-476a-a074-9748b2a55d17" />

### New `gh` experience table rendering experience

<img width="1166" alt="Screenshot 2025-03-13 at 5 53 14 PM" src="https://github.com/user-attachments/assets/360ccee0-dd8c-4366-b8cc-1bef0a959e91" />
<img width="1153" alt="Screenshot 2025-03-13 at 5 53 38 PM" src="https://github.com/user-attachments/assets/7964dd70-b81e-42d1-86da-4dca4ecab962" />


cc: @williammartin 


</p>
</details> 
author_association: CONTRIBUTOR
user: login: andyfeller
created_at: 2025-03-20T21:39:17Z
body: @williammartin : I'm going to spend some time later tonight putting together a demo of the changes in the PR just so we can confirm the experience meets expectations beyond the tests and code review.  Though the fix for table wrapping hasn't been released, I think it's okay we pin to the commit for now to get this merged.

So re-re-requesting review and I'll follow up on demoing it later tonight.

**UPDATE: See PR body above for updated screenshots**
author_association: CONTRIBUTOR
user: login: andyfeller
created_at: 2025-03-21T02:34:29Z
body: Want to give a special shout out to our friends at [`charmbracelet`](https://github.com/charmbracelet/) who helped make this work possible with work on `glamour` and `lipgloss` in no specific order:

- @andreynering
- @aymanbagabas
- @bashbunni
- @caarlos0
- @meowgorithm

author_association: CONTRIBUTOR