Skip to content
 
 

Repository files navigation

Note

This is nrzimmer's personal fork of dlvhdr/gh-dash, adding:

  • a generic, config-driven client-side filtering mechanism (extraFields + localFilter) for PR/issue sections — see Custom local filtering;
  • in-app section management keybindings (reload config, create a tab from the current search, edit/rename a tab's config without leaving the dashboard) — see In-app section management.

Everything else in this README describes the upstream project.

Keeping the gh dash extension on this fork's build: if you also use gh dash (the gh CLI extension) alongside the standalone gh-dash binary, note that they're two independent installs — building this repo with go install . only updates ~/.go/bin/gh-dash, not the extension. gh extension list will show the extension as dlvhdr/gh-dash even after you've manually swapped its binary for a fork build; that's just leftover manifest metadata, not upstream code actually running.

To make gh dash run this fork's build:

go install .
cp ~/.go/bin/gh-dash ~/.local/share/gh/extensions/gh-dash/gh-dash

Then pin the extension so gh extension upgrade doesn't silently overwrite it with an upstream release (this fork's changes aren't merged upstream, so an upgrade would revert them):

# gh >= ~2.30 exposes this directly:
gh extension pin gh-dash
# older gh (e.g. 2.94.0 as tested here) has no `pin` subcommand — edit the
# manifest field directly instead, which upgrade respects the same way:
sed -i 's/ispinned: false/ispinned: true/' ~/.local/share/gh/extensions/gh-dash/manifest.yml

Re-run the go install + cp step after every fork change you want reflected in gh dash — there's no build-on-demand wiring, it's a plain copied binary.


Text changing depending on mode. Light: 'So light!' Dark: 'So dark!'

→ https://gh-dash.dev ←

A rich terminal UI for GitHub that doesn't break your flow.

Latest Release Discord


❤️ Sponsoring

If you enjoy DASH and want to help, consider supporting the project with a donation at the sponsors page.

Thank you to all past and existing sponsors! 🙏🏽

Sponsors

TUI Visionary

Architect   Peter Steinberger  

TUI Power User

Alexander Garber   Brend Smits   Brian Gianforcaro   cdxn   Ken Sanders   Luke Rollans   Matthew Chisolm   Nikolay Kolev   Philippe Serhal   Scott Ames   sideshowbarker   Spencer Judd   Stefan Lüdin   Stevie Huh   Ulrich Dangel   vosahloj   Will Cory  

TUI Backer

Jeff Wainwright   purajit  

🌟 Features

  • User-defined, per-repo, PRs & issues sections
  • Overridable vim-style keyboard hotkeys
  • Custom actions to perform your specific workflow needs
  • Everything you can do on GitHub - diff, comment, checkout, push, update etc.
  • Control every setting with a YAML config file

If you like quickly navigating with your keyboard, seeing the PRs and issues you need and you love the terminal - DASH is for you! 🫵🏽

🧩 Custom local filtering (extraFields + localFilter)

This section documents a feature added in this fork, not present upstream.

Upstream DASH sections are filtered entirely by GitHub's search API (the filters field). That API has no qualifiers for things like "has a merge conflict", "has an unresolved review comment", or a Project's custom Status/Priority fields — so those can't be expressed as a filters string alone.

This fork adds two optional fields to prSections and issuesSections (and to the generic section config) that run a second, client-side filtering pass after the normal search:

  • extraFields — a raw GraphQL selection set, injected into the PR/Issue fragment of an unpaginated query used only for filtering. Any field valid on GitHub's PullRequest or Issue type can be requested here — nothing is hardcoded.
  • localFilter — a boolean expr-lang/expr expression, evaluated against the raw JSON node fetched via extraFields (plus the item's number). Items for which it evaluates to false are dropped from the section.

filters still runs first and does all the heavy lifting server-side (fewer items to fetch and filter); localFilter only needs to express what filters structurally can't.

Example 1 — PRs with a merge conflict or an unresolved review comment

prSections:
  - title: Conflict or review pending
    filters: is:open author:@me
    extraFields: |
      mergeable
      reviewThreads(last: 50) { nodes { isResolved } }
    localFilter: >-
      mergeable == "CONFLICTING" or any(reviewThreads.nodes, {.isResolved == false})

Example 2 — PRs from teammates that are stale and need a nudge

Approximates "not approved, and either never reviewed by me or reviewed before the last commit":

prSections:
  - title: Stale (push toward merge)
    filters: >-
      is:open
      -author:@me
      repo:my-org/my-repo
      updated:{{ nowModify "-60d" }}..{{ nowModify "-1d" }}
    extraFields: |
      reviewDecision
      reviews(last: 50) { nodes { author { login } submittedAt } }
      commits(last: 1) { nodes { commit { committedDate } } }
    localFilter: >-
      reviewDecision != "APPROVED"
      and not any(reviews.nodes, {.author.login == @me and .submittedAt >= (len(commits.nodes) > 0 ? commits.nodes[0].commit.committedDate : "")})

@me works in localFilter the same way it does in filters — it's replaced with your logged-in GitHub login (already known to the app, no extra API call) before the expression is compiled, so @me above is exactly equivalent to hardcoding "YOUR_LOGIN", just portable across machines/accounts. It only matches as a whole token (@meta is left alone), and if the login isn't known yet (a brief window right at startup) it safely resolves to an empty string instead of crashing.

Example 3 — PRs where you left a draft (PENDING) review you never submitted

Pending reviews aren't indexed by GitHub search at all, so this is only possible via localFilter:

prSections:
  - title: Unsubmitted reviews
    filters: is:open repo:my-org/my-repo
    extraFields: |
      reviews(last: 50) { nodes { author { login } state } }
    localFilter: >-
      any(reviews.nodes, {.author.login == @me and .state == "PENDING"})

Example 4 — Filtering by a GitHub Projects (v2) custom field

Status/Priority and other Project custom fields aren't reachable via filters at all, but they are just more GraphQL fields on the underlying Issue/PR (projectItems(...).fieldValueByName(...)), so extraFields can pull them in:

issuesSections:
  - title: "[Project] In Progress"
    filters: project:my-org/41 -is:closed
    limit: 300 # localFilter only sees the first `limit` items — raise it for large boards
    extraFields: |
      projectItems(first: 10) {
        nodes {
          project { number }
          status: fieldValueByName(name: "Status") { ... on ProjectV2ItemFieldSingleSelectValue { name } }
        }
      }
    localFilter: >-
      any(projectItems.nodes, {.project.number == 41 and .status != nil and .status.name == "In Progress"})

Notes / current limitations

  • localFilter evaluates against at most limit items per section (default 20, see defaults.prsLimit/defaults.issuesLimit). For large scopes (e.g. a whole Projects board), set an explicit limit: high enough to cover them — values above GitHub's 100-per-page cap are paginated internally, so e.g. limit: 300 works.
  • A section's displayed/total count reflects the post-localFilter count when localFilter is set (i.e. what you actually see in the list), not the raw pre-filter search count.
  • expr-lang/expr syntax notes that came up while writing these: use any(array, {.field == x}) / all(...) for existence checks (not a # lambda parameter), and guard optional fields with != nil before accessing a sub-field (.status != nil and .status.name == "Done", not .status.name == "Done" alone) since GraphQL returns null for unset fields.
  • When a section has localFilter set, its current value is shown as a small localFilter: ... line right below the search bar, so it doesn't get forgotten.

⌨️ In-app section management (Ctrl+R, Ctrl+T, Ctrl+E)

This section documents features added in this fork, not present upstream.

Upstream DASH only ever reads config.yml once at startup, and has no way to create or edit a section from the running dashboard — every change means quitting, editing YAML by hand, and restarting. This fork adds three keybindings (remappable like any other, via keybindings.universal with the builtin names below) that read and write the same config.yml the running dashboard is using, live:

  • Ctrl+R (builtin reloadConfig) — re-reads config.yml and re-applies theme/keybindings/ sections immediately, without restarting. A config with a syntax error is reported via the regular error line instead of crashing the dashboard; the previous, still-valid config keeps running.
  • Ctrl+T (builtin newTabFromSearch) — takes the filter currently active in the focused section (including the ephemeral global-search tab) and appends it as a new, permanent section to prSections/issuesSections under a title you type. Available in the PRs and Issues views.
  • Ctrl+E (builtin editSectionFilter) — opens a small in-TUI form (Tab/Shift+Tab to move between fields, Ctrl+S to save, Esc to cancel) to edit the focused section's Title, Filters, Limit, ExtraFields, and LocalFilter — including renaming the tab. Not available on the ephemeral global-search tab, which has no config.yml entry to edit.

All three write to whichever config file ParseConfig actually resolved for this run (the explicit --config path, GH_DASH_CONFIG, a repo-root .gh-dash.yml, or the global config), by patching the YAML text directly — comments and anchors (&name/*name, e.g. a shared extraFields block reused across sections) elsewhere in the file are preserved untouched. Only the field(s) that actually changed are rewritten, specifically so an untouched extraFields/ localFilter holding a shared anchor never gets silently dropped by an unrelated edit.

Known limitation: if a repo-local .gh-dash.yml and the global config both define prSections/issuesSections, the repo-local one wins wholesale (same merge rule as upstream) — writing to the global config in that case has no visible effect. Ctrl+E/rename also don't detect two sections sharing the same title in one list; the first match is used.

📃 Docs

DASH has an extensive docs site at gh-dash.dev/getting-started.

👥 Discord

Have questions? Join our Discord community!

🙏 Contributing

See the contribution guide at https://www.gh-dash.dev/contributing.

🛞 Under the hood

DASH uses:

Authors

Dolev Hadar (@dlvhdr) and the community.

About

A rich terminal UI for GitHub that doesn't break your flow.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages