Skip to content

Repository files navigation

Statline

codecov

GitHub team statistics in your terminal.

Statline (gh statline) is a terminal dashboard for the numbers behind your team's pull-request workflow — who's opening, merging, reviewing, approving, and commenting, across the repos your team actually works in.

Statline demo: team stats table, charts dashboard, and trends view

(All names in the demo are generated by the built-in seed command — no real data.)

Features

  • Team stats — one sortable stat line per member: PRs opened and merged, reviews given (approved / commented / changes requested), comments given vs received, median cycle time (open → merge), median time to first review, and median PR size. / finds a member by login in a long table.
  • Charts — a 3×3 dashboard that always fits one screen: PR throughput, review outcomes, cycle-time trend, who-reviews-whom matrix, first-review latency, PR sizes, open-PR aging, and an activity punch card, topped by stat tiles comparing the window to the previous one (▲/▼ deltas appear once the cache covers both). Any card expands to a scrollable fullscreen view; the review matrix pans with its name labels pinned, and o on the open-PR card opens a stale PR in your browser.
  • Trends — where the numbers are heading: a card per headline metric showing its 12-week trajectory with week-over-week deltas, plus a movers card for the members whose recent volume rose or fell the most ("reviews 2 → 8, up 4w running"). Cards expand to fullscreen — count metrics break down into per-member weekly sparklines, movers drop the top-3 cap. Weekly buckets are fixed regardless of the time window, and the series only reaches as far back as the cache honestly covers.
  • Person drill-down — headline stats, daily activity sparkline, and a per-repo breakdown for any teammate.
  • Time windows — cycle 7/14/30/90-day presets with w, or pick a custom date range with r.
  • Repo filterR narrows every view to a subset of the team's repos for per-repo standups and retros; the header and the export heading say which. The list scrolls, / searches it by name, and a/n check or clear what the search shows.
  • Teams your way — a setup wizard imports a GitHub org team (members + assigned repos) into a local config you can edit freely: add contractors, hide alumni (m in the app does it too), track repos the team isn't formally assigned. Multiple team profiles, switchable in-app with t, where a runs the wizard again for a new one.
  • Local cache — incremental sync into SQLite (pure Go, no CGO): instant startup, offline browsing, no re-fetching what you already have. A headless gh statline sync keeps the cache warm from cron.
  • Sync statusS in the app, or gh statline doctor from a script: when each repo last synced cleanly, how far back the cache honestly covers, and the error from any repo that is failing. A repo that stops syncing — renamed, made private, deleted — otherwise shows up only as numbers that quietly stop moving, so the status bar carries a persistent warning while any repo is failing.
  • Exporty copies the current view as a Markdown table for standups, retros, and 1:1 notes; gh statline export does the same headlessly, as Markdown, CSV or JSON, for a spreadsheet, a dashboard or a cron job. Every format renders from the same data, so a column means the same thing wherever you read it.
  • Keyboard-first (vim keys + arrows) with clickable tabs and rows, wheel scrolling, and an adaptive light/dark Charm-style theme.

Install

As a GitHub CLI extension (recommended):

gh extension install byte2pixel/gh-statline
gh statline

Standalone binaries are attached to each release (download, rename to gh-statline, put it on your PATH), or build from source with go install github.com/byte2pixel/gh-statline@latest.

Auth & scopes

Statline reuses your GitHub CLI credentials (gh auth login), falling back to GITHUB_TOKEN. It needs the repo and read:org scopes.

The host follows gh: GH_HOST if set, otherwise the single host gh is logged in to, otherwise github.com. Both the token lookup and the API endpoint use it, so statline finds a GitHub Enterprise Server login instead of reporting missing credentials. The queries are tested against github.com alone, so GHES is best effort. Reports welcome.

Usage

gh statline              # open the TUI (first run launches the setup wizard)
gh statline init         # add another team profile (or press a in the team switcher)
gh statline sync         # refresh the cache without the TUI (cron-friendly)
gh statline sync --team platform --backfill 180
gh statline doctor       # per-repo sync health, straight from the cache
gh statline export       # print a view as Markdown, CSV or JSON
gh statline --version    # the release you are running
gh statline --config ./team.yml --db ./team.db sync   # other files, on any command

doctor reports, for every configured repo, when it last synced cleanly, how far back the cache honestly covers, and why the last sync failed. It never contacts GitHub, so it works offline and on local-only teams, and it exits non-zero when any repo is failing — pair it with sync in cron to catch a repo that has quietly stopped updating (renamed, made private, or deleted) instead of trusting numbers that stopped moving.

Export

gh statline export --view team --format csv --window 30d
gh statline export --view person --member alice --format json
gh statline export --view trends --output trends.md
gh statline export --view sync --format json | jq '.repos[] | select(.last_error)'
Flag
--view team (default), person, trends, sync
--format md (default), csv, json
--window days, e.g. 30d; defaults to your configured ui.window
--from / --to a custom YYYY-MM-DD range instead of --window
--member the login to drill into (required by --view person)
--team team profile; defaults to default_team
--output, -o write to a file instead of stdout

Like doctor, it reads the cache only: offline, no GitHub call, local-only teams included. --format md is exactly what y copies in the app. The trends view is always the trailing 12 weeks, so it ignores --window, as the trends page does.

For the machine formats:

  • CSV and JSON name their columns identically, using stable machine keys (prs_opened, cycle_time_p50_seconds) rather than the display headings, so rewording a column in the UI never renames it in your spreadsheet.
  • Durations are whole seconds, timestamps are RFC 3339 UTC.
  • "No data" is null in JSON and an empty CSV field, never the 0 or -1 the metrics layer uses internally, which a dashboard would average in as a measurement.
  • A view made of two tables (a person's totals and their repo breakdown; the weekly trend and its movers) writes both: two JSON arrays, or two CSV blocks separated by a blank line.

For cron, sync --json prints one object instead of progress lines — what the run updated, and the state every repo is left in — and doctor --json prints the health report on its own. Both use the same per-repo field names as export --view sync, and both still exit non-zero when a repo is failing:

gh statline sync --json | jq -r '.repos[] | select(.last_error) | .repo'

Keys

Key Action
1 / 2 / 3 / tab Team stats / charts / trends
enter / esc Drill into member / back
j/k, arrows Move selection
h/l, / Change sort column
- Flip sort direction
/ Find a member by login (enter keeps the rows, esc clears)
f / enter Expand the focused card (charts, trends)
j/k · h/l Scroll / pan a fullscreen card
o Open the marked PR in your browser (Open PRs card)
pgup/pgdn/space, d/u, g/G Page / half-page / jump in a fullscreen card
w Cycle time window
r Custom date range
t Switch team (a there adds one, d deletes)
m Show or hide members (writes hidden: to the config)
R Filter repos
s Sync now; again while syncing to cancel
S Sync status (per-repo health)
y Copy view as Markdown
? Full help
q Quit

Configuration

Lives at ~/.config/gh-statline/config.yml (Windows: %AppData%\gh-statline\config.yml) — wizard-written, human-editable:

default_team: platform
exclude_bots: ["*[bot]", "dependabot*", "renovate*", "copilot*"]
teams:
  - name: platform
    org: acme
    gh_team_slug: platform-eng   # provenance of the import; optional
    # no_sync: true              # local-only profile; sync never touches GitHub
    members:
      - {login: alice}
      - {login: bob, hidden: true}   # kept in cache, hidden from views
    repos:
      - {owner: acme, name: api}
      - {owner: acme, name: web}
sync: {backfill_days: 120, page_size: 25, concurrency: 3}
ui: {window: 30d, sort: prs_merged, theme: auto}   # see below; theme is hand-edited

exclude_bots globs (*, ?, case-insensitive, brackets literal) match reviewer and commenter logins in addition to GitHub's own bot flag. copilot* is in the defaults because Copilot code review reviews as copilot-pull-request-reviewer, which has no [bot] suffix, so no other glob catches it. A Copilot review lands within minutes of the PR opening, and without the glob it would count as the first review whenever the synced bot flag missed it. Defaults only fill in when the key is absent from the file, so a config written by an older version keeps its old list. Add new globs by hand.

sync tunes the GitHub walk: backfill_days is how far back a fresh cache reaches, page_size is PRs per request (1–100), and concurrency is how many repos sync in parallel, capped at 10. GitHub's secondary rate limits trigger on concurrent requests from one token, so more workers than that earn a block, not a faster sync.

Statline remembers how you left it: switching teams (t) updates default_team, changing the time window (w) or the sort column (/) updates ui, and showing or hiding members (m) writes their hidden: flags, so the next launch reopens the same view. Custom date ranges (r), the repo filter (R), the member search (/), and --team <name> are one-shot and never persist. These in-app changes rewrite the file, so YAML comments don't survive a session — keep notes elsewhere if you hand-edit.

Statline picks its palette from the terminal's background color, which it asks for with an OSC 11 query. Terminals that never answer, among them older conhost and some tmux and CI setups, keep the dark-assumed default. On a light background that chrome is hard to read and the light chart ramps never engage. theme: light (or dark) pins the palette and skips the query, while auto or no key at all keeps asking. Nothing in the app writes this key, so it survives the rewrites above.

The SQLite cache lives in the user cache dir and is safe to delete — it just re-syncs.

Both files can live elsewhere: --config <path> and --db <path> work on every command, STATLINE_CONFIG and STATLINE_DB do the same from the environment, and the flag wins when both are set. That is how to keep a scratch profile, or the seeded demo team, away from your real config.

Metric definitions

  • Counts are attributed to the person acting: reviews to the reviewer, comments to their author. Commenting on your own PR never counts.
  • Review-thread replies arrive as GitHub "commented" reviews; v0.1 counts them as such (a known inflation of the commented bucket).
  • Reviews GitHub has since marked dismissed still count as reviews given, in their own column: the review happened, and a later push invalidating an approval shouldn't erase the reviewer's work.
  • Time to first review ignores bots and the PR author.
  • Hidden members and bots are excluded as actors from every number, including the charts: what the team table shows and what the charts total are the same set of people. Their activity towards a visible member still counts, so hiding a teammate never rewrites someone else's stat line.
  • Medians use the lower-middle value; a means no data in the window.
  • Percent changes (tiles, trends, movers) are rounded to the nearest whole percent, with one shared definition. A change from a zero base has no percentage and is labelled new; in the movers ranking, new activity sorts ahead of every percentage change, by volume.
  • The review matrix only counts reviews on PRs authored by humans: reviews on bot-authored PRs are excluded, and the (others) column (reviews on non-member PRs) doesn't influence the heat-map scale.
  • Windows and dates are UTC everywhere, with two deliberate exceptions. The punch card buckets by local time, so two people in different timezones read different punch cards from the same cache. Rate-limit resets print in local time with the zone attached, because they answer "when can I retry" rather than "when did this happen".
  • The updatedAt-ordered incremental walk cannot see PRs untouched since before the backfill horizon (default 120 days) — deepen with sync --backfill N. Fetched data is never deleted, so local coverage grows the longer you use statline; the tile deltas turn on per window once the cache provably covers the previous period.

Development

go build ./...   # pure Go, no C toolchain needed
go test ./...
go run .
go run . seed    # dev helper: seeds a deterministic local-only "demo" team
                 # (38 members, 120d of history) to inspect charts at scale

To render views headlessly (sizes, scroll states, seeded data), use the dump harness: STATLINE_DUMP=1 go test ./internal/tui/app -run TestDumpView -v — see dump_test.go for the STATLINE_DUMP_VIEW/_PRE/_POST options.

The README demo GIF is scripted with VHS against seeded data — vhs/demo.tape has the Docker one-liner to regenerate it after UI changes.

Built with Bubble Tea v2, Lip Gloss, Bubbles, ntcharts, BubbleZone, Harmonica, go-gh, and modernc.org/sqlite.

License

MIT

About

Statline, your GitHub team-stats TUI

Topics

Resources

Contributing

Security policy

Stars

5 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages