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-dashThen 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.ymlRe-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.
A rich terminal UI for GitHub that doesn't break your flow.
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! 🙏🏽
- 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! 🫵🏽
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'sPullRequestorIssuetype can be requested here — nothing is hardcoded.localFilter— a boolean expr-lang/expr expression, evaluated against the raw JSON node fetched viaextraFields(plus the item'snumber). Items for which it evaluates tofalseare 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.
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})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.
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"})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"})localFilterevaluates against at mostlimititems per section (default 20, seedefaults.prsLimit/defaults.issuesLimit). For large scopes (e.g. a whole Projects board), set an explicitlimit:high enough to cover them — values above GitHub's 100-per-page cap are paginated internally, so e.g.limit: 300works.- A section's displayed/total count reflects the post-
localFiltercount whenlocalFilteris set (i.e. what you actually see in the list), not the raw pre-filter search count. expr-lang/exprsyntax notes that came up while writing these: useany(array, {.field == x})/all(...)for existence checks (not a#lambda parameter), and guard optional fields with!= nilbefore accessing a sub-field (.status != nil and .status.name == "Done", not.status.name == "Done"alone) since GraphQL returnsnullfor unset fields.- When a section has
localFilterset, its current value is shown as a smalllocalFilter: ...line right below the search bar, so it doesn't get forgotten.
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(builtinreloadConfig) — re-readsconfig.ymland 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(builtinnewTabFromSearch) — takes the filter currently active in the focused section (including the ephemeral global-search tab) and appends it as a new, permanent section toprSections/issuesSectionsunder a title you type. Available in the PRs and Issues views.Ctrl+E(builtineditSectionFilter) — opens a small in-TUI form (Tab/Shift+Tabto move between fields,Ctrl+Sto save,Escto cancel) to edit the focused section'sTitle,Filters,Limit,ExtraFields, andLocalFilter— including renaming the tab. Not available on the ephemeral global-search tab, which has noconfig.ymlentry 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.
DASH has an extensive docs site at gh-dash.dev/getting-started.
Have questions? Join our Discord community!
See the contribution guide at https://www.gh-dash.dev/contributing.
DASH uses: