[
  {
    "path": ".gitattributes",
    "content": "* linguist-detectable=false\n*.go linguist-detectable=true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help improve prs\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**Setup**\nPlease complete the following information along with version numbers, if applicable.\n - `prs` version or the git commit that you've built `prs` on\n - OS [e.g. Ubuntu, macOS]\n - Shell [e.g. zsh, fish]\n - Terminal Emulator [e.g. kitty, iterm]\n - Terminal Multiplexer [e.g. tmux]\n - Locale [e.g. en_US.UTF-8, zh_CN.UTF-8, etc.]\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. ...\n2. ...\n3. Error occurs\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nAdd screenshots to help explain your problem.\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for prs\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"monthly\"\n    groups:\n      patch-updates:\n        update-types: [\"patch\"]\n      minor-updates:\n        update-types: [\"minor\"]\n    labels:\n      - \"dependencies\"\n    commit-message:\n      prefix: \"build\"\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"monthly\"\n    labels:\n      - \"dependencies\"\n    commit-message:\n      prefix: \"ci\"\n"
  },
  {
    "path": ".github/scripts/get-yamlfmt.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\nif [ $# -ne 3 ]; then\n    echo \"Usage: $0 <os> <arch> <version>\"\n    echo \"eg: $0 Linux x86_64 0.13.0\"\n    exit 1\nfi\n\nOS=\"$1\"\nARCH=\"$2\"\nVERSION=\"$3\"\n\ncwd=$(pwd)\n\ntemp_dir=$(mktemp -d)\nif [ ! -e ${temp_dir} ]; then\n    echo \"Failed to create temporary directory.\"\n    exit 1\nfi\n\ncd $temp_dir\n\ncurl -sSLO \"https://github.com/google/yamlfmt/releases/download/v${VERSION}/yamlfmt_${VERSION}_${OS}_${ARCH}.tar.gz\"\ncurl -sSLO \"https://github.com/google/yamlfmt/releases/download/v${VERSION}/checksums.txt\"\n\nsha256sum --ignore-missing -c checksums.txt\n\ntar -xzf \"yamlfmt_${VERSION}_${OS}_${ARCH}.tar.gz\" -C ${temp_dir}/\ncd $cwd\n\ncp \"${temp_dir}/yamlfmt\" .\n\nrm -r ${temp_dir}\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: main\n\non:\n  push:\n    branches:\n      - 'main'\n\njobs:\n  changes:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n    outputs:\n      code: ${{ steps.filter.outputs.code }}\n      deps: ${{ steps.filter.outputs.deps }}\n      release: ${{ steps.filter.outputs.release }}\n      workflows: ${{ steps.filter.outputs.workflows }}\n      yml: ${{ steps.filter.outputs.yml }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n      - uses: dorny/paths-filter@v4\n        id: filter\n        with:\n          filters: |\n            code:\n              - \"cmd/**\"\n              - \"internal/**\"\n              - \"ui/**\"\n              - \"**/*.go\"\n              - \"go.*\"\n              - \".golangci.yml\"\n              - \"main.go\"\n              - \".github/actions/**\"\n              - \".github/workflows/main.yml\"\n            deps:\n              - \"go.mod\"\n              - \"go.sum\"\n              - \".github/workflows/main.yml\"\n            release:\n              - \".goreleaser.yaml\"\n              - \".github/workflows/main.yml\"\n            workflows:\n              - \".github/workflows/**.yml\"\n            yml:\n              - \"**.yml\"\n              - \"**.yaml\"\n\n  lint:\n    needs: changes\n    if: ${{ needs.changes.outputs.code == 'true' }}\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: dhth/composite-actions/.github/actions/lint-go@main\n\n  build:\n    needs: changes\n    if: ${{ needs.changes.outputs.code == 'true' }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v6\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: 'go.mod'\n      - name: go build\n        run: go build -v ./...\n\n  test:\n    needs: changes\n    if: ${{ needs.changes.outputs.code == 'true' }}\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: 'go.mod'\n      - name: go test\n        run: go test -v ./...\n\n  lint-yaml:\n    needs: changes\n    if: ${{ needs.changes.outputs.yml == 'true' }}\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n      - uses: dhth/composite-actions/.github/actions/lint-yaml@main\n\n  lint-workflows:\n    needs: changes\n    if: ${{ needs.changes.outputs.workflows == 'true' }}\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n      - uses: dhth/composite-actions/.github/actions/lint-actions@main\n\n  release-check:\n    needs: changes\n    if: ${{ needs.changes.outputs.release == 'true' }}\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: 'go.mod'\n      - name: Release check\n        uses: goreleaser/goreleaser-action@v7\n        with:\n          version: 'v2.9.0'\n          args: check\n\n  vulncheck:\n    needs: changes\n    if: ${{ needs.changes.outputs.deps == 'true' }}\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: 'go.mod'\n      - name: install govulncheck\n        run: go install golang.org/x/vuln/cmd/govulncheck@latest\n      - name: govulncheck\n        run: govulncheck ./...\n"
  },
  {
    "path": ".github/workflows/pr.yml",
    "content": "name: pr\n\non:\n  pull_request:\n\njobs:\n  changes:\n    runs-on: ubuntu-latest\n    permissions:\n      pull-requests: read\n    outputs:\n      code: ${{ steps.filter.outputs.code }}\n      deps: ${{ steps.filter.outputs.deps }}\n      release: ${{ steps.filter.outputs.release }}\n      workflows: ${{ steps.filter.outputs.workflows }}\n      yml: ${{ steps.filter.outputs.yml }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n      - uses: dorny/paths-filter@v4\n        id: filter\n        with:\n          filters: |\n            code:\n              - \"cmd/**\"\n              - \"internal/**\"\n              - \"ui/**\"\n              - \"**/*.go\"\n              - \"go.*\"\n              - \".golangci.yml\"\n              - \"main.go\"\n              - \".github/actions/**\"\n              - \".github/workflows/pr.yml\"\n            deps:\n              - \"go.mod\"\n              - \"go.sum\"\n              - \".github/workflows/pr.yml\"\n            release:\n              - \".goreleaser.yaml\"\n              - \".github/workflows/pr.yml\"\n            workflows:\n              - \".github/workflows/**.yml\"\n            yml:\n              - \"**.yml\"\n              - \"**.yaml\"\n\n  lint:\n    needs: changes\n    if: ${{ needs.changes.outputs.code == 'true' }}\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: dhth/composite-actions/.github/actions/lint-go@main\n\n  build:\n    needs: changes\n    if: ${{ needs.changes.outputs.code == 'true' }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v6\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: 'go.mod'\n      - name: go build\n        run: go build -v ./...\n\n  test:\n    needs: changes\n    if: ${{ needs.changes.outputs.code == 'true' }}\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: 'go.mod'\n      - name: go test\n        run: go test -v ./...\n\n  lint-yaml:\n    needs: changes\n    if: ${{ needs.changes.outputs.yml == 'true' }}\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n      - uses: dhth/composite-actions/.github/actions/lint-yaml@main\n\n  lint-workflows:\n    needs: changes\n    if: ${{ needs.changes.outputs.workflows == 'true' }}\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n      - uses: dhth/composite-actions/.github/actions/lint-actions@main\n\n  release-check:\n    needs: changes\n    if: ${{ needs.changes.outputs.release == 'true' }}\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: 'go.mod'\n      - name: Release check\n        uses: goreleaser/goreleaser-action@v7\n        with:\n          version: 'v2.9.0'\n          args: check\n\n  vulncheck:\n    needs: changes\n    if: ${{ needs.changes.outputs.deps == 'true' }}\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: 'go.mod'\n      - name: install govulncheck\n        run: go install golang.org/x/vuln/cmd/govulncheck@latest\n      - name: govulncheck\n        run: govulncheck ./...\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: release\n\non:\n  push:\n    tags:\n      - 'v*'\n\npermissions:\n  id-token: write\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: 'go.mod'\n      - name: Install Cosign\n        uses: sigstore/cosign-installer@v3\n        with:\n          cosign-release: 'v2.5.0'\n      - name: Release Binaries\n        uses: goreleaser/goreleaser-action@v7\n        with:\n          version: 'v2.9.0'\n          args: release --clean\n        env:\n          GITHUB_TOKEN: ${{secrets.GH_PAT}}\n"
  },
  {
    "path": ".github/workflows/scan.yml",
    "content": "name: scan\n\non:\n  workflow_dispatch:\n  schedule:\n    - cron: '0 8 10 * *'\n\njobs:\n  virus-total:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: 'go.mod'\n      - name: Build Binaries\n        uses: goreleaser/goreleaser-action@v7\n        with:\n          version: 'v2.9.0'\n          args: build --snapshot\n      - name: List binaries\n        run: |\n          ls -lh ./dist/prs_*/prs\n      - uses: dhth/composite-actions/.github/actions/scan-files@main\n        with:\n          files: './dist/prs_*/prs'\n          vt-api-key: ${{ secrets.VT_API_KEY }}\n"
  },
  {
    "path": ".github/workflows/vulncheck.yml",
    "content": "name: vulncheck\n\non:\n  workflow_dispatch:\n  schedule:\n    - cron: '0 2 * * 2,6'\n\njobs:\n  vulncheck:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: 'go.mod'\n      - name: install govulncheck\n        run: go install golang.org/x/vuln/cmd/govulncheck@latest\n      - name: govulncheck\n        run: govulncheck ./...\n"
  },
  {
    "path": ".gitignore",
    "content": "prs\n.quickrun\ncosign.key\njustfile\ndebug.log\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\nlinters:\n  enable:\n    - errname\n    - errorlint\n    - goconst\n    - nilerr\n    - prealloc\n    - predeclared\n    - revive\n    - testifylint\n    - thelper\n    - unconvert\n    - usestdlibvars\n    - wastedassign\n  settings:\n    revive:\n      rules:\n        - name: blank-imports\n        - name: context-as-argument\n          arguments:\n            - allowTypesBefore: '*testing.T'\n        - name: context-keys-type\n        - name: dot-imports\n        - name: empty-block\n        - name: error-naming\n        - name: error-return\n        - name: error-strings\n        - name: errorf\n        - name: exported\n        - name: if-return\n        - name: increment-decrement\n        - name: indent-error-flow\n        - name: package-comments\n        - name: range\n        - name: receiver-naming\n        - name: redefines-builtin-id\n        - name: superfluous-else\n        - name: time-naming\n        - name: unexported-return\n        - name: unreachable-code\n        - name: unused-parameter\n        - name: var-declaration\n        - name: unnecessary-stmt\n        - name: confusing-naming\n        - name: unused-receiver\n        - name: unhandled-error\n          arguments:\n            - fmt.Print\n            - fmt.Println\n            - fmt.Printf\n            - fmt.Fprintf\n            - fmt.Fprint\n  exclusions:\n    generated: lax\n    presets:\n      - comments\n      - common-false-positives\n      - legacy\n      - std-error-handling\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\nformatters:\n  enable:\n    - gofumpt\n  exclusions:\n    generated: lax\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": ".goreleaser.yaml",
    "content": "version: 2\n\nrelease:\n  draft: true\n\nbefore:\n  hooks:\n    - go mod tidy\n    - go generate ./...\n\nbuilds:\n  - env:\n      - CGO_ENABLED=0\n    goos:\n      - linux\n      - darwin\n    goarch:\n      - amd64\n      - arm64\n\nsigns:\n  - cmd: cosign\n    signature: \"${artifact}.sig\"\n    certificate: \"${artifact}.pem\"\n    args:\n      - \"sign-blob\"\n      - \"--oidc-issuer=https://token.actions.githubusercontent.com\"\n      - \"--output-certificate=${certificate}\"\n      - \"--output-signature=${signature}\"\n      - \"${artifact}\"\n      - \"--yes\"\n    artifacts: checksum\n\nbrews:\n  - name: prs\n    repository:\n      owner: dhth\n      name: homebrew-tap\n    directory: Formula\n    license: MIT\n    homepage: \"https://github.com/dhth/prs\"\n    description: \"Stay updated on pull requests from your terminal\"\n\nchangelog:\n  sort: asc\n  filters:\n    exclude:\n      - \"^docs:\"\n      - \"^test:\"\n      - \"^ci:\"\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# AGENTS.md\n\nThis file provides guidance to AI coding agents when working with code in this repository.\n\n## Project Overview\n\n`prs` is a Go terminal UI application for browsing GitHub pull requests. It uses the Bubble Tea TUI framework with GitHub GraphQL API integration via `gh` CLI authentication.\n\n## Common Commands\n\n```bash\njust build      # go build -ldflags='-s -w' .\njust run        # go run .\njust install    # go install -ldflags='-s -w' .\njust lint       # golangci-lint run\njust fmt        # gofumpt -w .\n```\n\nNote: always use `just` to run commands.\n\n## Architecture\n\nThe app follows the standard **Bubble Tea** (Elm-architecture) pattern: `Model` → `Update(msg)` → `View()` cycle.\n\n**Entry flow**: `main.go` → `cmd.Execute()` (Cobra CLI) → `ui.RenderUI()` → `tea.NewProgram(InitialModel())`\n\n### Key packages\n\n- **`cmd/`** — Cobra CLI setup, config loading (Viper). Config priority: flags > env vars (`PRS_*`) > config file.\n- **`ui/`** — All TUI logic, split by concern:\n  - `model.go` — Central Bubble Tea `Model` struct (state)\n  - `update.go` — Event handling (largest file)\n  - `view.go` — View rendering\n  - `cmds.go` — Async `tea.Cmd` functions (GitHub API calls)\n  - `msgs.go` — Message types passed through Bubble Tea\n  - `gh.go` — GitHub GraphQL queries\n  - `types.go` — GraphQL query/response type definitions\n  - `navigation.go` — View/section navigation with back-stack (`activePane`, `lastPane`, `secondLastActivePane`)\n  - `styles.go` / `colors.go` — Lipgloss terminal styling\n  - `render_helpers.go` — Display formatting helpers\n- **`internal/utils/`** — Markdown rendering with Glamour (embedded Gruvbox theme)\n\n### View system\n\nSix panes managed by `activePane` enum: `repoListView`, `prListView`, `prDetailsView`, `prTLListView`, `prTLItemDetailView`, `helpView`.\n\nPR details has sub-sections: metadata, description, checks, references, files, commits, comments.\n\n### Caching\n\nPR details and timeline data are cached in maps keyed by `\"owner/repo:number\"` to avoid redundant API calls.\n\n## Linting\n\nUses golangci-lint v2 with `gofumpt` formatter and `revive` linter (among others). See `.golangci.yml` for full config.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024-2026 Dhruv Thakur\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <h1 align=\"center\">prs</h1>\n  <p align=\"center\">\n    <a href=\"https://github.com/dhth/prs/actions/workflows/main.yml\"><img alt=\"build status\" src=\"https://img.shields.io/github/actions/workflow/status/dhth/prs/main.yml?style=flat-square\"></a>\n    <a href=\"https://github.com/dhth/prs/actions/workflows/vulncheck.yml\"><img alt=\"vuln check\" src=\"https://img.shields.io/github/actions/workflow/status/dhth/prs/vulncheck.yml?style=flat-square&label=vulncheck\"></a>\n    <a href=\"https://github.com/dhth/prs/releases/latest\"><img alt=\"Latest release\" src=\"https://img.shields.io/github/release/dhth/prs.svg?style=flat-square\"></a>\n    <a href=\"https://github.com/dhth/prs/releases/latest\"><img alt=\"Commits since latest release\" src=\"https://img.shields.io/github/commits-since/dhth/prs/latest?style=flat-square\"></a>\n  </p>\n</p>\n\n`prs` lets you stay updated on pull requests from your terminal.\n\n<p align=\"center\">\n  <img src=\"https://tools.dhruvs.space/images/prs/v1-0-0/prs.gif\" alt=\"Usage\" />\n</p>\n\n[source video](https://youtu.be/H81ru9cQhDo)\n\n🤔 Motivation\n---\n\nFor my day job as a tech lead, I need to stay updated on several PRs, and my\nhope is that `prs` will let me do that faster than the Github web UI (or other\ntools for that matter).\n\n💾 Installation\n---\n\n**homebrew**:\n\n```sh\nbrew install dhth/tap/prs\n```\n\n**go**:\n\n```sh\ngo install github.com/dhth/prs@latest\n```\n\nOr get the binaries directly from a [release][3]. Read more about verifying the\nauthenticity of released artifacts [here](#-verifying-release-artifacts).\n\n🔑 Authentication\n---\n\nYou can have `prs` make authenticated calls to GitHub on your behalf in either\nof two ways:\n\n- Have an authenticated instance of [gh](https://github.com/cli/cli) available\n    (recommended).\n- Provide a valid Github token via `$GH_TOKEN`.\n\n⚡️ Usage\n---\n\n`prs` has two modes:\n\n- **Query mode** (default): lets you search PRs based on a query you provide (based\n  on github's [search\n  syntax](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests))\n- **Repos mode**: let you pick a repository from a predefined list\n\n### Query Mode\n\n```shell\nprs --query='type:pr repo:neovim/neovim state:open label:lua linked:issue'\n\n# view open PRs where you're the author\nprs -q 'type:pr author:@me state:open'\n\n# view open PRs where a review has been requested from you\nPRS_QUERY='type:pr user-review-requested:@me state:open' prs\n\n# read query from prs' config file\nprs\n```\n\n### Repos Mode\n\n```shell\nprs --mode=repos --repos='dhth/prs,dhth/omm,dhth/hours'\n\nPRS_REPOS='dhth/prs,dhth/omm,dhth/hours' prs --mode=repos\n\n# read repos from prs' config file\nprs -m repos\n```\n\n🛠️ Configuration\n---\n\n`prs` accepts configuration from any of the following:\n\n- Command line flags (run `prs -h` for details)\n- Environment variables (eg. `PRS_QUERY`)\n- `prs`'s config file, which looks like this:\n\n    ```yaml\n    num: 20\n    repos:\n      - dhth/omm\n      - dhth/hours\n      - dhth/prs\n      - neovim/neovim\n      - junegunn/fzf\n      - BurntSushi/ripgrep\n      - charmbracelet/bubbletea\n      - goreleaser/goreleaser\n      - dandavison/delta\n    query: 'type:pr repo:neovim/neovim state:open label:lua linked:issue'\n    ```\n\nFor every configuration property, the order of priority is: `flag >>\nenvironment variables >> config file`, ie, flags take the highest priority.\n\n**[`^ back to top ^`](#prs)**\n\nScreenshots\n---\n\n### PR List View\n\n![Screen 1](https://tools.dhruvs.space/images/prs/v1-0-0/prs-1.png)\n\n### PR Timeline List View\n\n![Screen 2](https://tools.dhruvs.space/images/prs/v1-0-0/prs-2.png)\n\n### PR Timeline Item Detail View\n![Screen 3](https://tools.dhruvs.space/images/prs/v1-0-0/prs-3.png)\n\n### PR Details View\n\n![Screen 4](https://tools.dhruvs.space/images/prs/v1-0-0/prs-4.png)\n\n![Screen 5](https://tools.dhruvs.space/images/prs/v1-0-0/prs-5.png)\n\n![Screen 6](https://tools.dhruvs.space/images/prs/v1-0-0/prs-6.png)\n\n![Screen 7](https://tools.dhruvs.space/images/prs/v1-0-0/prs-7.png)\n\n**[`^ back to top ^`](#prs)**\n\nKeyboard Shortcuts\n---\n\n### General\n\n```text\n  q/esc/ctrl+c                      go back\n  Q                                 quit from anywhere\n  ?                                 Open Help View\n  d                                 Open PR Details View\n  ctrl+v                            Show PR details using gh\n```\n\n### PR List View\n\n```text\n  Indicators for current review decision:\n\n  ±  implies                        CHANGES_REQUESTED\n  🟡 implies                        REVIEW_REQUIRED\n  ✅ implies                        APPROVED\n\n  ⏎/tab/shift+tab/2                 Switch focus to PR Timeline View\n  ctrl+s                            Switch focus to Repo List View (when --mode=repos)\n  ctrl+d                            Show PR diff\n  ctrl+r                            Reload PR list\n  ctrl+b                            Open PR in browser\n```\n\n### PR Details View\n\n```text\n  h/N/←                             Go to previous section\n  l/n/→                             Go to next section\n  1/2/3...                          Go to specific section\n  J/]                               Go to next PR\n  K/[                               Go to previous PR\n  d                                 Go back to last view\n  ctrl+b                            Open PR in browser\n```\n\n### Timeline List View\n\n\n```text\n  tab/shift+tab/1                   Switch focus to PR List View\n  ⏎/3                               Show details for PR timeline item (when applicable)\n  ctrl+d                            Show PR diff\n  ctrl+b                            Open timeline item in browser\n  ctrl+r                            Reload PR timeline\n```\n\n### Timeline Item Detail View\n\n\n```text\n  1                                 Switch focus to PR List View\n  2                                 Switch focus to PR Timeline List View\n  ctrl+d                            Show PR diff\n  ctrl+b                            Open timeline item in browser\n  h/N/←                             Go to previous section\n  l/n/→                             Go to next section\n```\n\n### 🔐 Verifying release artifacts\n\nIn case you get the `prs` binary directly from a [release][3], you may want to\nverify its authenticity. Checksums are applied to all released artifacts, and\nthe resulting checksum file is signed using\n[cosign](https://docs.sigstore.dev/cosign/installation/).\n\nSteps to verify (replace the version in the commands listed with the one you\nwant):\n\n1. Download the following files from the release:\n\n   - prs_1.0.0_checksums.txt\n   - prs_1.0.0_checksums.txt.pem\n   - prs_1.0.0_checksums.txt.sig\n\n2. Verify the signature:\n\n   ```shell\n   cosign verify-blob prs_1.0.0_checksums.txt \\\n       --certificate prs_1.0.0_checksums.txt.pem \\\n       --signature prs_1.0.0_checksums.txt.sig \\\n       --certificate-identity-regexp 'https://github\\.com/dhth/prs/\\.github/workflows/.+' \\\n       --certificate-oidc-issuer \"https://token.actions.githubusercontent.com\"\n   ```\n\n3. Download the compressed archive you want, and validate its checksum:\n\n   ```shell\n   curl -sSLO https://github.com/dhth/prs/releases/download/v1.0.0/prs_1.0.0_linux_amd64.tar.gz\n   sha256sum --ignore-missing -c prs_1.0.0_checksums.txt\n   ```\n\n3. If checksum validation goes through, uncompress the archive:\n\n   ```shell\n   tar -xzf prs_1.0.0_linux_amd64.tar.gz\n   ./prs\n   # profit!\n   ```\n\nAcknowledgements\n---\n\n`prs` is built using [bubbletea][1], and released via [goreleaser][2].\n\n[1]: https://github.com/charmbracelet/bubbletea\n[2]: https://github.com/goreleaser/goreleaser\n[3]: https://github.com/dhth/prs/releases\n\n**[`^ back to top ^`](#prs)**\n"
  },
  {
    "path": "cmd/config.go",
    "content": "package cmd\n\nimport (\n\t\"os\"\n\t\"os/user\"\n\t\"strings\"\n)\n\nfunc expandTilde(path string) string {\n\tif strings.HasPrefix(path, \"~\") {\n\t\tusr, err := user.Current()\n\t\tif err != nil {\n\t\t\tos.Exit(1)\n\t\t}\n\t\treturn strings.Replace(path, \"~\", usr.HomeDir, 1)\n\t}\n\treturn path\n}\n"
  },
  {
    "path": "cmd/root.go",
    "content": "package cmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\tghapi \"github.com/cli/go-gh/v2/pkg/api\"\n\t\"github.com/dhth/prs/ui\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/pflag\"\n\t\"github.com/spf13/viper\"\n)\n\nconst (\n\tenvPrefix          = \"PRS\"\n\tauthor             = \"@dhth\"\n\tprojectHomePage    = \"https://github.com/dhth/prs\"\n\tissuesURL          = \"https://github.com/dhth/prs/issues\"\n\tconfigFileName     = \"prs/prs.yml\"\n\tdefaultSearchQuery = \"type:pr author:@me sort:updated-desc state:open\"\n\tdefaultPRNum       = 20\n\tmaxPRNum           = 50\n)\n\nvar (\n\terrCouldntGetHomeDir        = errors.New(\"couldn't get home directory\")\n\terrCouldntGetConfigDir      = errors.New(\"couldn't get config directory\")\n\terrModeIncorrect            = errors.New(\"incorrect mode provided\")\n\terrConfigFileDoesntExist    = errors.New(\"config file does not exist\")\n\terrNoReposProvided          = errors.New(\"no repos were provided\")\n\terrIncorrectRepoProvided    = errors.New(\"incorrect repo provided\")\n\terrCouldntSetupGithubClient = errors.New(\"couldn't set up a Github Client\")\n)\n\nvar reportIssueMsg = fmt.Sprintf(\"Let %s know about this error via %s.\", author, issuesURL)\n\nfunc Execute(version string) error {\n\trootCmd, err := NewRootCommand(version)\n\tif err != nil {\n\t\tfmt.Printf(\"Error: %s\\n\", err.Error())\n\t\tswitch {\n\t\tcase errors.Is(err, errCouldntGetHomeDir), errors.Is(err, errCouldntGetConfigDir):\n\t\t\tfmt.Printf(`\nThis is a fatal error; use --config-path to specify config file path manually.\n%s\n`, reportIssueMsg)\n\t\t}\n\t\treturn err\n\t}\n\n\terr = rootCmd.Execute()\n\n\tif errors.Is(err, errCouldntSetupGithubClient) {\n\t\tfmt.Printf(`\nIf the error is due to misconfigured authentication, you can fix that by either of the following:\n- Provide a valid Github token via $GH_TOKEN\n- Have an authenticated instance of gh (https://github.com/cli/cli) available\n`)\n\t}\n\treturn err\n}\n\nfunc NewRootCommand(version string) (*cobra.Command, error) {\n\tvar (\n\t\tconfigFilePath string\n\t\tconfigPathFull string\n\t\tmode           ui.Mode\n\t\tmodeInp        string\n\t\trepoStrs       []string\n\t\trepos          []ui.Repo\n\t\tsearchQuery    string\n\t\tghClient       *ghapi.GraphQLClient\n\t\tprNum          int\n\t)\n\n\trootCmd := &cobra.Command{\n\t\tUse:   \"prs\",\n\t\tShort: \"prs lets you stay updated on pull requests from your terminal\",\n\t\tLong: fmt.Sprintf(`prs lets you stay updated on pull requests from your terminal.\n\nUse it to query for specific pull requests based on a filter query (using\nGithub's search syntax), or have it let you pick a repository from a predefined\nlist.\n\nExamples:\n$ prs --query='type:pr repo:neovim/neovim state:open label:lua linked:issue'\n$ prs -q 'type:pr author:@me state:open'\n$ PRS_QUERY='type:pr user-review-requested:@me state:open' prs\n$ prs # will read query from config file\n\n$ prs --mode=repos --repos='dhth/prs,dhth/omm,dhth/hours'\n$ PRS_REPOS='dhth/prs,dhth/omm,dhth/hours' prs --mode=repos\n$ prs -m repos # will read repos from config file\n\nProject home page: %s\n`, projectHomePage),\n\n\t\tArgs:         cobra.MaximumNArgs(0),\n\t\tSilenceUsage: true,\n\t\tVersion:      version,\n\t\tPersistentPreRunE: func(cmd *cobra.Command, _ []string) error {\n\t\t\tconfigPathFull = expandTilde(configFilePath)\n\n\t\t\tif filepath.Ext(configPathFull) != \".yml\" {\n\t\t\t\treturn errConfigFileDoesntExist\n\t\t\t}\n\t\t\t_, err := os.Stat(configPathFull)\n\n\t\t\tfl := cmd.Flags()\n\t\t\tif fl != nil {\n\t\t\t\tcf := fl.Lookup(\"config-path\")\n\t\t\t\tif cf != nil && cf.Changed && errors.Is(err, fs.ErrNotExist) {\n\t\t\t\t\treturn errConfigFileDoesntExist\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar v *viper.Viper\n\t\t\tv, err = initializeConfig(cmd, configPathFull)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif prNum > maxPRNum {\n\t\t\t\tprNum = maxPRNum\n\t\t\t}\n\n\t\t\tswitch modeInp {\n\t\t\tcase \"repos\":\n\t\t\t\tmode = ui.RepoMode\n\t\t\tcase \"query\":\n\t\t\t\tmode = ui.QueryMode\n\t\t\tdefault:\n\t\t\t\treturn errModeIncorrect\n\t\t\t}\n\n\t\t\tif mode == ui.RepoMode {\n\t\t\t\tvar reposToUse []string\n\t\t\t\t// pretty ugly hack to get around the fact that\n\t\t\t\t// v.GetStringSlice(\"repos\") always seems to prioritize the config file\n\t\t\t\tif len(repoStrs) > 0 && len(repoStrs[0]) > 0 && !strings.HasPrefix(repoStrs[0], \"[\") {\n\t\t\t\t\treposToUse = repoStrs\n\t\t\t\t} else {\n\t\t\t\t\treposToUse = v.GetStringSlice(\"repos\")\n\t\t\t\t}\n\n\t\t\t\tif len(reposToUse) == 0 {\n\t\t\t\t\treturn errNoReposProvided\n\t\t\t\t}\n\n\t\t\t\tfor _, r := range reposToUse {\n\t\t\t\t\trepoEls := strings.Split(r, \"/\")\n\t\t\t\t\t// TODO: there can be more validations done here, maybe regex based\n\t\t\t\t\tif len(repoEls) != 2 {\n\t\t\t\t\t\treturn fmt.Errorf(\"%w: %s\", errIncorrectRepoProvided, r)\n\t\t\t\t\t}\n\n\t\t\t\t\trepos = append(repos, ui.Repo{\n\t\t\t\t\t\tOwner: strings.TrimSpace(repoEls[0]),\n\t\t\t\t\t\tName:  strings.TrimSpace(repoEls[1]),\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\n\t\t\topts := ghapi.ClientOptions{\n\t\t\t\tEnableCache: true,\n\t\t\t\tCacheTTL:    time.Second * 30,\n\t\t\t\tTimeout:     8 * time.Second,\n\t\t\t}\n\n\t\t\tghClient, err = ghapi.NewGraphQLClient(opts)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"%w: %s\", errCouldntSetupGithubClient, err.Error())\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t\tRunE: func(_ *cobra.Command, _ []string) error {\n\t\t\tconfig := ui.Config{\n\t\t\t\tPRCount: prNum,\n\t\t\t\tRepos:   repos,\n\t\t\t\tQuery:   &searchQuery,\n\t\t\t}\n\t\t\treturn ui.RenderUI(ghClient, config, mode)\n\t\t},\n\t}\n\trootCmd.SetVersionTemplate(`{{with .Name}}{{printf \"%s \" .}}{{end}}{{printf \"%s\" .Version}}\n`)\n\n\tros := runtime.GOOS\n\tuserCfgDir, err := os.UserConfigDir()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%w: %s\", errCouldntGetConfigDir, err.Error())\n\t}\n\n\tvar defaultConfigFilePath string\n\tswitch ros {\n\tcase \"linux\", \"windows\":\n\t\tdefaultConfigFilePath = filepath.Join(userCfgDir, configFileName)\n\tdefault:\n\t\t// to use ~/.config instead of $HOME/Library/Application Support\n\t\thd, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"%w: %s\", errCouldntGetHomeDir, err.Error())\n\t\t}\n\t\tdefaultConfigFilePath = filepath.Join(hd, \".config\", configFileName)\n\t}\n\n\trootCmd.Flags().StringVarP(&configFilePath, \"config-path\", \"c\", defaultConfigFilePath, \"location of prs's config file\")\n\trootCmd.Flags().StringVarP(&modeInp, \"mode\", \"m\", \"query\", \"mode to run prs in; values: query, repos\")\n\trootCmd.Flags().StringVarP(&searchQuery, \"query\", \"q\", defaultSearchQuery, \"query to search PRs for\")\n\trootCmd.Flags().IntVarP(&prNum, \"num\", \"n\", defaultPRNum, \"number of PRs to fetch\")\n\trootCmd.Flags().StringSliceVarP(&repoStrs, \"repos\", \"r\", nil, \"comma separated list of repos to use for repo mode\")\n\n\trootCmd.CompletionOptions.DisableDefaultCmd = true\n\n\treturn rootCmd, nil\n}\n\nfunc initializeConfig(cmd *cobra.Command, configFile string) (*viper.Viper, error) {\n\tv := viper.New()\n\n\tv.SetConfigName(filepath.Base(configFile))\n\tv.SetConfigType(\"yaml\")\n\tv.AddConfigPath(filepath.Dir(configFile))\n\n\terr := v.ReadInConfig()\n\tif err != nil && !errors.As(err, &viper.ConfigFileNotFoundError{}) {\n\t\treturn v, err\n\t}\n\n\tv.SetEnvPrefix(envPrefix)\n\tv.SetEnvKeyReplacer(strings.NewReplacer(\"-\", \"_\"))\n\tv.AutomaticEnv()\n\n\terr = bindFlags(cmd, v)\n\tif err != nil {\n\t\treturn v, err\n\t}\n\n\treturn v, nil\n}\n\nfunc bindFlags(cmd *cobra.Command, v *viper.Viper) error {\n\tvar err error\n\tcmd.Flags().VisitAll(func(f *pflag.Flag) {\n\t\tif !f.Changed && v.IsSet(f.Name) {\n\t\t\tval := v.Get(f.Name)\n\t\t\tfErr := cmd.Flags().Set(f.Name, fmt.Sprintf(\"%v\", val))\n\t\t\tif fErr != nil {\n\t\t\t\terr = fErr\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t})\n\treturn err\n}\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3.8'\n\n# to test workings on linux\nservices:\n  prs-dev:\n    image: golang:1.23-alpine\n    volumes:\n      - .:/go/src/app\n    working_dir: /go/src/app\n    command: sleep infinity\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/dhth/prs\n\ngo 1.26.3\n\nrequire (\n\tcharm.land/bubbles/v2 v2.1.0\n\tcharm.land/bubbletea/v2 v2.0.6\n\tcharm.land/lipgloss/v2 v2.0.3\n\tgithub.com/charmbracelet/glamour v1.0.0\n\tgithub.com/cli/go-gh/v2 v2.13.0\n\tgithub.com/cli/shurcooL-graphql v0.0.4\n\tgithub.com/dustin/go-humanize v1.0.1\n\tgithub.com/muesli/termenv v0.16.0\n\tgithub.com/spf13/cobra v1.10.2\n\tgithub.com/spf13/pflag v1.0.10\n\tgithub.com/spf13/viper v1.21.0\n\tgithub.com/stretchr/testify v1.11.1\n)\n\nrequire (\n\tgithub.com/alecthomas/chroma/v2 v2.20.0 // indirect\n\tgithub.com/atotto/clipboard v0.1.4 // indirect\n\tgithub.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect\n\tgithub.com/aymerick/douceur v0.2.0 // indirect\n\tgithub.com/charmbracelet/colorprofile v0.4.3 // indirect\n\tgithub.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect\n\tgithub.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 // indirect\n\tgithub.com/charmbracelet/x/ansi v0.11.7 // indirect\n\tgithub.com/charmbracelet/x/cellbuf v0.0.15 // indirect\n\tgithub.com/charmbracelet/x/exp/slice v0.0.0-20250812135814-932da4e322f4 // indirect\n\tgithub.com/charmbracelet/x/term v0.2.2 // indirect\n\tgithub.com/charmbracelet/x/termios v0.1.1 // indirect\n\tgithub.com/charmbracelet/x/windows v0.2.2 // indirect\n\tgithub.com/cli/safeexec v1.0.1 // indirect\n\tgithub.com/clipperhouse/displaywidth v0.11.0 // indirect\n\tgithub.com/clipperhouse/uax29/v2 v2.7.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/dlclark/regexp2 v1.11.5 // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.4.0 // indirect\n\tgithub.com/gorilla/css v1.0.1 // indirect\n\tgithub.com/henvic/httpretty v0.1.4 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.4.0 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.23 // indirect\n\tgithub.com/microcosm-cc/bluemonday v1.0.27 // indirect\n\tgithub.com/muesli/cancelreader v0.2.2 // indirect\n\tgithub.com/muesli/reflow v0.3.0 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/sagikazarmark/locafero v0.11.0 // indirect\n\tgithub.com/sahilm/fuzzy v0.1.1 // indirect\n\tgithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect\n\tgithub.com/spf13/afero v1.15.0 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/thlib/go-timezone-local v0.0.7 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgithub.com/yuin/goldmark v1.7.13 // indirect\n\tgithub.com/yuin/goldmark-emoji v1.0.6 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect\n\tgolang.org/x/net v0.43.0 // indirect\n\tgolang.org/x/sync v0.20.0 // indirect\n\tgolang.org/x/sys v0.43.0 // indirect\n\tgolang.org/x/term v0.36.0 // indirect\n\tgolang.org/x/text v0.30.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g=\ncharm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY=\ncharm.land/bubbletea/v2 v2.0.6 h1:UHN/91OyuhaOFGSrBXQ/hMZD8IO1Uc4BvHlgHXL2WJo=\ncharm.land/bubbletea/v2 v2.0.6/go.mod h1:MH/D8ZLlN3op37vQvijKuU29g3rqTp+aQapURFonF9g=\ncharm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU=\ncharm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA=\ngithub.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=\ngithub.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=\ngithub.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=\ngithub.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=\ngithub.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=\ngithub.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=\ngithub.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=\ngithub.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=\ngithub.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=\ngithub.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=\ngithub.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=\ngithub.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=\ngithub.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=\ngithub.com/charmbracelet/glamour v1.0.0 h1:AWMLOVFHTsysl4WV8T8QgkQ0s/ZNZo7CiE4WKhk8l08=\ngithub.com/charmbracelet/glamour v1.0.0/go.mod h1:DSdohgOBkMr2ZQNhw4LZxSGpx3SvpeujNoXrQyH2hxo=\ngithub.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=\ngithub.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=\ngithub.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 h1:Q9fO0y1Zo5KB/5Vu8JZoLGm1N3RzF9bNj3Ao3xoR+Ac=\ngithub.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468/go.mod h1:bAAz7dh/FTYfC+oiHavL4mX1tOIBZ0ZwYjSi3qE6ivM=\ngithub.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=\ngithub.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=\ngithub.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=\ngithub.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=\ngithub.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=\ngithub.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=\ngithub.com/charmbracelet/x/exp/slice v0.0.0-20250812135814-932da4e322f4 h1:00G7Z522s/WiCooTYFKATlN5Gjn64miEheomXCPwRcU=\ngithub.com/charmbracelet/x/exp/slice v0.0.0-20250812135814-932da4e322f4/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms=\ngithub.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=\ngithub.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=\ngithub.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=\ngithub.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=\ngithub.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=\ngithub.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=\ngithub.com/cli/go-gh/v2 v2.13.0 h1:jEHZu/VPVoIJkciK3pzZd3rbT8J90swsK5Ui4ewH1ys=\ngithub.com/cli/go-gh/v2 v2.13.0/go.mod h1:Us/NbQ8VNM0fdaILgoXSz6PKkV5PWaEzkJdc9vR2geM=\ngithub.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00=\ngithub.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=\ngithub.com/cli/shurcooL-graphql v0.0.4 h1:6MogPnQJLjKkaXPyGqPRXOI2qCsQdqNfUY1QSJu2GuY=\ngithub.com/cli/shurcooL-graphql v0.0.4/go.mod h1:3waN4u02FiZivIV+p1y4d0Jo1jc6BViMA73C+sZo2fk=\ngithub.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=\ngithub.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=\ngithub.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=\ngithub.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=\ngithub.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=\ngithub.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=\ngithub.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=\ngithub.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=\ngithub.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=\ngithub.com/henvic/httpretty v0.1.4 h1:Jo7uwIRWVFxkqOnErcoYfH90o3ddQyVrSANeS4cxYmU=\ngithub.com/henvic/httpretty v0.1.4/go.mod h1:Dn60sQTZfbt2dYsdUSNsCljyF4AfdqnuJFDLJA1I4AM=\ngithub.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=\ngithub.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=\ngithub.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=\ngithub.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=\ngithub.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=\ngithub.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=\ngithub.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=\ngithub.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=\ngithub.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\ngithub.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=\ngithub.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=\ngithub.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=\ngithub.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=\ngithub.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=\ngithub.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=\ngithub.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=\ngithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=\ngithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=\ngithub.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=\ngithub.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/thlib/go-timezone-local v0.0.7 h1:fX8zd3aJydqLlTs/TrROrIIdztzsdFV23OzOQx31jII=\ngithub.com/thlib/go-timezone-local v0.0.7/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngithub.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=\ngithub.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=\ngithub.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=\ngithub.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=\ngolang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=\ngolang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=\ngolang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=\ngolang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=\ngolang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=\ngolang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=\ngolang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=\ngolang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=\ngopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "internal/utils/assets/gruvbox.json",
    "content": "{\n  \"document\": {\n    \"block_prefix\": \"\",\n    \"block_suffix\": \"\",\n    \"color\": \"#fbf1c7\",\n    \"margin\": 2\n  },\n  \"block_quote\": {\n    \"indent\": 1,\n    \"indent_token\": \"┃ \"\n  },\n  \"paragraph\": {},\n  \"list\": {\n    \"level_indent\": 2\n  },\n  \"heading\": {\n    \"block_suffix\": \"\\n\",\n    \"color\": \"#fe8019\",\n    \"bold\": true\n  },\n  \"h1\": {\n    \"prefix\": \"# \",\n    \"suffix\": \"\",\n    \"color\": \"#d3869b\",\n    \"bold\": true\n  },\n  \"h2\": {\n    \"prefix\": \"## \",\n    \"color\": \"#83a598\"\n  },\n  \"h3\": {\n    \"prefix\": \"### \",\n    \"color\": \"#83a598\"\n  },\n  \"h4\": {\n    \"prefix\": \"#### \",\n    \"color\": \"#83a598\"\n  },\n  \"h5\": {\n    \"prefix\": \"\",\n    \"color\": \"#fabd2f\"\n  },\n  \"h6\": {\n    \"prefix\": \"\",\n    \"color\": \"#8ec07c\",\n    \"bold\": false\n  },\n  \"text\": {},\n  \"strikethrough\": {\n    \"crossed_out\": true\n  },\n  \"emph\": {\n    \"color\": \"#83a598\",\n    \"italic\": true,\n    \"prefix\": \" \",\n    \"suffix\": \" \"\n  },\n  \"strong\": {\n    \"color\": \"#fe8019\",\n    \"bold\": true\n  },\n  \"hr\": {\n    \"color\": \"#928374\",\n    \"format\": \"\\n--------\\n\"\n  },\n  \"item\": {\n    \"block_prefix\": \"• \"\n  },\n  \"enumeration\": {\n    \"block_prefix\": \". \"\n  },\n  \"task\": {\n    \"ticked\": \"[✓] \",\n    \"unticked\": \"[ ] \"\n  },\n  \"link\": {\n    \"color\": \"#83a598\",\n    \"underline\": true\n  },\n  \"link_text\": {\n    \"color\": \"#fabd2f\",\n    \"bold\": true\n  },\n  \"image\": {\n    \"color\": \"132\",\n    \"underline\": true\n  },\n  \"image_text\": {\n    \"color\": \"245\",\n    \"format\": \"Image: {{.text}} →\"\n  },\n  \"code\": {\n    \"color\": \"#fabd2f\",\n    \"bold\": false\n  },\n  \"code_block\": {\n    \"color\": \"244\",\n    \"chroma\": {\n      \"text\": {\n        \"color\": \"#fbf1c7\"\n      },\n      \"error\": {\n        \"color\": \"#282828\",\n        \"background_color\": \"#fb4934\"\n      },\n      \"comment\": {\n        \"color\": \"#928374\"\n      },\n      \"comment_preproc\": {\n        \"color\": \"#8ec07c\"\n      },\n      \"keyword\": {\n        \"color\": \"#fe8019\"\n      },\n      \"keyword_reserved\": {\n        \"color\": \"#8ec07c\"\n      },\n      \"keyword_namespace\": {\n        \"color\": \"#d3869b\"\n      },\n      \"keyword_type\": {\n        \"color\": \"#fabd2f\"\n      },\n      \"operator\": {\n        \"color\": \"#fe8019\"\n      },\n      \"punctuation\": {\n        \"color\": \"#928374\"\n      },\n      \"name\": {\n        \"color\": \"#ebdbb2\"\n      },\n      \"name_builtin\": {\n        \"color\": \"#fabd2f\"\n      },\n      \"name_tag\": {\n        \"color\": \"#fb4934\"\n      },\n      \"name_attribute\": {\n        \"color\": \"#b8bb26\"\n      },\n      \"name_class\": {\n        \"color\": \"#fe8019\"\n      },\n      \"name_constant\": {\n        \"color\": \"#d3869b\"\n      },\n      \"name_decorator\": {\n        \"color\": \"#d3869b\"\n      },\n      \"name_exception\": {\n        \"color\": \"#fb4934\"\n      },\n      \"name_function\": {\n        \"color\": \"#fabd2f\"\n      },\n      \"name_other\": {},\n      \"literal\": {},\n      \"literal_number\": {\n        \"color\": \"#fabd2f\"\n      },\n      \"literal_date\": {},\n      \"literal_string\": {\n        \"color\": \"#b8bb26\"\n      },\n      \"literal_string_escape\": {\n        \"color\": \"#83a598\"\n      },\n      \"generic_deleted\": {\n        \"color\": \"#fb4934\"\n      },\n      \"generic_emph\": {\n        \"color\": \"#83a598\",\n        \"italic\": true\n      },\n      \"generic_inserted\": {\n        \"color\": \"#b8bb26\"\n      },\n      \"generic_strong\": {\n        \"color\": \"#ebdbb2\",\n        \"bold\": true\n      },\n      \"generic_subheading\": {\n        \"color\": \"#b8bb26\"\n      },\n      \"background\": {\n        \"background_color\": \"#282828\"\n      }\n    }\n  },\n  \"table\": {\n    \"center_separator\": \"┼\",\n    \"column_separator\": \"│\",\n    \"row_separator\": \"─\"\n  },\n  \"definition_list\": {},\n  \"definition_term\": {},\n  \"definition_description\": {\n    \"block_prefix\": \"\\n🠶 \"\n  },\n  \"html_block\": {},\n  \"html_span\": {}\n}\n"
  },
  {
    "path": "internal/utils/markdown.go",
    "content": "package utils\n\nimport (\n\t_ \"embed\"\n\n\t\"github.com/charmbracelet/glamour\"\n\t\"github.com/muesli/termenv\"\n)\n\n//go:embed assets/gruvbox.json\nvar glamourJSONBytes []byte\n\nfunc GetMarkDownRenderer(wrap int) (*glamour.TermRenderer, error) {\n\treturn glamour.NewTermRenderer(\n\t\tglamour.WithStylesFromJSONBytes(glamourJSONBytes),\n\t\tglamour.WithColorProfile(termenv.TrueColor),\n\t\tglamour.WithWordWrap(wrap),\n\t)\n}\n"
  },
  {
    "path": "internal/utils/markdown_test.go",
    "content": "package utils\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/charmbracelet/glamour\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetGlamourStyleFromFile(t *testing.T) {\n\tgotOption := glamour.WithStylesFromJSONBytes(glamourJSONBytes)\n\trenderer, err := glamour.NewTermRenderer(gotOption)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, renderer)\n\n\t_, err = renderer.Render(\"a\")\n\tassert.NoError(t, err)\n}\n\nfunc TestGlamourStylesFileIsValid(t *testing.T) {\n\tgot := json.Valid(glamourJSONBytes)\n\tassert.True(t, got)\n}\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"os\"\n\t\"runtime/debug\"\n\n\t\"github.com/dhth/prs/cmd\"\n)\n\nvar version = \"dev\"\n\nfunc main() {\n\tv := version\n\tif version == \"dev\" {\n\t\tinfo, ok := debug.ReadBuildInfo()\n\t\tif ok {\n\t\t\tv = info.Main.Version\n\t\t}\n\t}\n\terr := cmd.Execute(v)\n\tif err != nil {\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "ui/assets/help.md",
    "content": "# prs Reference Manual\n\n(scroll line by line with j/k/arrow keys or by half a page with c-d/c-u)\n\n## Views\n\nprs has 6 views:\n\n- PR List View\n- PR Details View\n- PR Timeline List View\n- PR Timeline Item Detail View\n- Repo List View (only applicable when --mode=repos)\n- Help View (this one)\n\n## Keyboard Shortcuts\n\n### General\n\n```text\n  q/esc/ctrl+c                      go back\n  Q                                 quit from anywhere\n  ?                                 Open Help View\n  d                                 Open PR Details View\n  ctrl+v                            Show PR details using gh\n```\n\n### PR List View\n\n```text\n  Indicators for current review decision:\n\n  ±  implies                        CHANGES_REQUESTED\n  🟡 implies                        REVIEW_REQUIRED\n  ✅ implies                        APPROVED\n\n  ⏎/tab/shift+tab/2                 Switch focus to PR Timeline View\n  ctrl+s                            Switch focus to Repo List View (when --mode=repos)\n  ctrl+d                            Show PR diff\n  ctrl+r                            Reload PR list\n  ctrl+b                            Open PR in browser\n```\n\n### PR Details View\n\n```text\n  h/N/←                             Go to previous section\n  l/n/→                             Go to next section\n  1/2/3...                          Go to specific section\n  J/]                               Go to next PR\n  K/[                               Go to previous PR\n  d                                 Go back to last view\n  ctrl+b                            Open PR in browser\n```\n\n### Timeline List View\n\n\n```text\n  tab/shift+tab/1                   Switch focus to PR List View\n  ⏎/3                               Show details for PR timeline item (when applicable)\n  ctrl+d                            Show PR diff\n  ctrl+b                            Open timeline item in browser\n  ctrl+r                            Reload PR timeline\n```\n\n### Timeline Item Detail View\n\n\n```text\n  1                                 Switch focus to PR List View\n  2                                 Switch focus to PR Timeline List View\n  ctrl+d                            Show PR diff\n  ctrl+b                            Open timeline item in browser\n  h/N/←                             Go to previous section\n  l/n/→                             Go to next section\n```\n"
  },
  {
    "path": "ui/cmds.go",
    "content": "package ui\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"time\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\tghapi \"github.com/cli/go-gh/v2/pkg/api\"\n)\n\nvar errOSNotSupported = errors.New(\"OS not supported\")\n\nfunc chooseRepo(repo string) tea.Cmd {\n\treturn func() tea.Msg {\n\t\treturn repoChosenMsg{repo}\n\t}\n}\n\nfunc openURLInBrowser(url string) tea.Cmd {\n\treturn func() tea.Msg {\n\t\tvar openCmd string\n\t\tvar args []string\n\n\t\tswitch runtime.GOOS {\n\t\tcase \"darwin\":\n\t\t\topenCmd = \"open\"\n\t\t\targs = []string{url}\n\t\tcase \"linux\":\n\t\t\topenCmd = \"xdg-open\"\n\t\t\targs = []string{url}\n\t\tcase \"windows\":\n\t\t\topenCmd = \"rundll32\"\n\t\t\targs = []string{\"url.dll,FileProtocolHandler\", url}\n\t\tdefault:\n\t\t\treturn urlOpenedinBrowserMsg{url: url, err: errOSNotSupported}\n\t\t}\n\n\t\tc := exec.Command(openCmd, args...)\n\t\terr := c.Run()\n\n\t\treturn urlOpenedinBrowserMsg{url: url, err: err}\n\t}\n}\n\nfunc showDiff(repoOwner, repoName string, prNumber int) tea.Cmd {\n\tcmd := []string{\n\t\t\"gh\",\n\t\t\"--repo\",\n\t\tfmt.Sprintf(\"%s/%s\", repoOwner, repoName),\n\t\t\"pr\",\n\t\t\"diff\",\n\t\tfmt.Sprintf(\"%d\", prNumber),\n\t}\n\tc := exec.Command(cmd[0], cmd[1:]...)\n\n\treturn tea.ExecProcess(c, func(err error) tea.Msg {\n\t\tif err != nil {\n\t\t\treturn prDiffDoneMsg{err: err}\n\t\t}\n\t\treturn tea.Msg(prDiffDoneMsg{})\n\t})\n}\n\nfunc showPR(repoOwner, repoName string, prNumber int) tea.Cmd {\n\tcmd := []string{\n\t\t\"gh\",\n\t\t\"--repo\",\n\t\tfmt.Sprintf(\"%s/%s\", repoOwner, repoName),\n\t\t\"pr\",\n\t\t\"view\",\n\t\t\"--comments\",\n\t\tfmt.Sprintf(\"%d\", prNumber),\n\t}\n\tc := exec.Command(cmd[0], cmd[1:]...)\n\n\treturn tea.ExecProcess(c, func(err error) tea.Msg {\n\t\tif err != nil {\n\t\t\treturn prDiffDoneMsg{err: err}\n\t\t}\n\t\treturn tea.Msg(prDiffDoneMsg{})\n\t})\n}\n\nfunc hideHelp(interval time.Duration) tea.Cmd {\n\treturn tea.Tick(interval, func(time.Time) tea.Msg {\n\t\treturn hideHelpMsg{}\n\t})\n}\n\nfunc fetchPRSFromQuery(ghClient *ghapi.GraphQLClient, queryStr string, prCount int) tea.Cmd {\n\treturn func() tea.Msg {\n\t\tprs, err := getPRDataFromQuery(ghClient, queryStr, prCount)\n\t\treturn prsFetchedMsg{prs, err}\n\t}\n}\n\nfunc fetchPRSForRepo(ghClient *ghapi.GraphQLClient, repoOwner string, repoName string, prCount int) tea.Cmd {\n\treturn func() tea.Msg {\n\t\tqueryStr := fmt.Sprintf(\"type:pr repo:%s/%s sort:updated-desc\", repoOwner, repoName)\n\t\tprs, err := getPRDataFromQuery(ghClient, queryStr, prCount)\n\t\treturn prsFetchedMsg{prs, err}\n\t}\n}\n\nfunc fetchPRMetadata(ghClient *ghapi.GraphQLClient, repoOwner, repoName string, prNumber int) tea.Cmd {\n\treturn func() tea.Msg {\n\t\tmetadata, err := getPRMetadata(ghClient, repoOwner, repoName, prNumber)\n\t\treturn prMetadataFetchedMsg{repoOwner, repoName, prNumber, metadata, err}\n\t}\n}\n\nfunc fetchPRTLItems(ghClient *ghapi.GraphQLClient, repoOwner string, repoName string, prNumber int, tlItemsCount int, setItems bool) tea.Cmd {\n\treturn func() tea.Msg {\n\t\tprTLItems, err := getPRTLData(ghClient, repoOwner, repoName, prNumber, tlItemsCount)\n\t\treturn prTLFetchedMsg{repoOwner, repoName, prNumber, prTLItems, setItems, err}\n\t}\n}\n"
  },
  {
    "path": "ui/colors.go",
    "content": "package ui\n\nvar colors = []string{\n\t\"#fe77a4\",\n\t\"#d3869a\",\n\t\"#ff4c8b\",\n\t\"#ffb0c2\",\n\t\"#df748b\",\n\t\"#ff6682\",\n\t\"#f19597\",\n\t\"#d89e9d\",\n\t\"#fc5260\",\n\t\"#e96462\",\n\t\"#ffb5a2\",\n\t\"#febcac\",\n\t\"#f0947b\",\n\t\"#ff6334\",\n\t\"#af9084\",\n\t\"#ff5405\",\n\t\"#e98658\",\n\t\"#be876e\",\n\t\"#ff803b\",\n\t\"#fd780b\",\n\t\"#ff9743\",\n\t\"#e2ac85\",\n\t\"#d67717\",\n\t\"#d4925c\",\n\t\"#ffb472\",\n\t\"#fe9103\",\n\t\"#de9644\",\n\t\"#dc8b00\",\n\t\"#ffb13c\",\n\t\"#c9b094\",\n\t\"#faca7d\",\n\t\"#c7921f\",\n\t\"#c6a267\",\n\t\"#d3cdc5\",\n\t\"#fabd2f\",\n\t\"#dcad50\",\n\t\"#daa402\",\n\t\"#ffc20c\",\n\t\"#fbcf56\",\n\t\"#b29807\",\n\t\"#e7c727\",\n\t\"#c7b648\",\n\t\"#9c9360\",\n\t\"#cec48b\",\n\t\"#bbb206\",\n\t\"#ddd601\",\n\t\"#d1cc74\",\n\t\"#b8bb26\",\n\t\"#acaa5e\",\n\t\"#b4c800\",\n\t\"#a6b92b\",\n\t\"#a8b64c\",\n\t\"#aab08a\",\n\t\"#849843\",\n\t\"#a8d906\",\n\t\"#a8a9a3\",\n\t\"#88b500\",\n\t\"#add562\",\n\t\"#a0d845\",\n\t\"#8de107\",\n\t\"#829b60\",\n\t\"#7db839\",\n\t\"#94bc63\",\n\t\"#71c200\",\n\t\"#b5d092\",\n\t\"#6e9f3a\",\n\t\"#51a100\",\n\t\"#b5e48c\",\n\t\"#8ce852\",\n\t\"#59d412\",\n\t\"#89d967\",\n\t\"#59c435\",\n\t\"#4ba539\",\n\t\"#00b700\",\n\t\"#00db04\",\n\t\"#9ae089\",\n\t\"#6fbd63\",\n\t\"#83b87a\",\n\t\"#5ddb63\",\n\t\"#04eb4d\",\n\t\"#7a9879\",\n\t\"#00ce48\",\n\t\"#05b64c\",\n\t\"#9cdea5\",\n\t\"#64d97f\",\n\t\"#8fbc96\",\n\t\"#4daa67\",\n\t\"#00d977\",\n\t\"#12b667\",\n\t\"#6ed999\",\n\t\"#63bd8f\",\n\t\"#00d990\",\n\t\"#a7e0c2\",\n\t\"#0abe88\",\n\t\"#90b4a6\",\n\t\"#83a598\",\n\t\"#5cab95\",\n\t\"#b9d9cf\",\n\t\"#03d7b3\",\n\t\"#00b499\",\n\t\"#6fd0bd\",\n\t\"#1edacd\",\n\t\"#19b7b2\",\n\t\"#89cbce\",\n\t\"#4dcfdb\",\n\t\"#62a6ae\",\n\t\"#90e1ef\",\n\t\"#01aac0\",\n\t\"#48cae4\",\n\t\"#00ddff\",\n\t\"#6ac1db\",\n\t\"#00c3f9\",\n\t\"#99bbcd\",\n\t\"#149ccd\",\n\t\"#6da2c6\",\n\t\"#7bcaff\",\n\t\"#07b1fa\",\n\t\"#b4d4fb\",\n\t\"#629fdb\",\n\t\"#5aaaff\",\n\t\"#0798ff\",\n\t\"#4896ef\",\n\t\"#bbd1ff\",\n\t\"#9fb9f0\",\n\t\"#949aab\",\n\t\"#7b8ad5\",\n\t\"#8498fb\",\n\t\"#aaaffc\",\n\t\"#8187dc\",\n\t\"#ada7ff\",\n\t\"#aba3ca\",\n\t\"#d2c8f2\",\n\t\"#a681fb\",\n\t\"#b798f0\",\n\t\"#c3a4e1\",\n\t\"#ce8cf7\",\n\t\"#c97df9\",\n\t\"#e6a5f4\",\n\t\"#e47cfb\",\n\t\"#ffc6ff\",\n\t\"#f344ff\",\n\t\"#a882a7\",\n\t\"#c57fbf\",\n\t\"#ff4ded\",\n\t\"#f081de\",\n\t\"#fc69e6\",\n\t\"#dfa5ca\",\n\t\"#f646c1\",\n\t\"#ceb4c3\",\n\t\"#f27abe\",\n\t\"#ae8c99\",\n\t\"#ee91b6\",\n}\n"
  },
  {
    "path": "ui/gh.go",
    "content": "package ui\n\nimport (\n\t\"log\"\n\n\tghapi \"github.com/cli/go-gh/v2/pkg/api\"\n\tghgql \"github.com/cli/shurcooL-graphql\"\n)\n\nfunc getPRDataFromQuery(ghClient *ghapi.GraphQLClient, queryStr string, prCount int) ([]pr, error) {\n\tvar query prSearchQuery\n\n\tvariables := map[string]any{\n\t\t\"query\": ghgql.String(queryStr),\n\t\t\"count\": ghgql.Int(prCount),\n\t}\n\terr := ghClient.Query(\"PRQuery\", &query, variables)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar prs []pr //nolint:prealloc\n\tfor _, edge := range query.Search.Edges {\n\t\tif edge.Node.Type != \"PullRequest\" {\n\t\t\tcontinue\n\t\t}\n\t\tprs = append(prs, edge.Node.pr)\n\t}\n\treturn prs, nil\n}\n\nfunc getPRMetadata(ghClient *ghapi.GraphQLClient, repoOwner string, repoName string, prNumber int) (prDetails, error) {\n\tvar query prDetailsQuery\n\n\tvariables := map[string]any{\n\t\t\"repositoryOwner\":          ghgql.String(repoOwner),\n\t\t\"repositoryName\":           ghgql.String(repoName),\n\t\t\"pullRequestNumber\":        ghgql.Int(prNumber),\n\t\t\"reviewRequestsCount\":      ghgql.Int(reviewRequestsCount),\n\t\t\"latestReviewsCount\":       ghgql.Int(latestReviewsCount),\n\t\t\"filesCount\":               ghgql.Int(filesCount),\n\t\t\"labelsCount\":              ghgql.Int(labelsCount),\n\t\t\"assigneesCount\":           ghgql.Int(assigneesCount),\n\t\t\"issuesCount\":              ghgql.Int(issuesCount),\n\t\t\"participantsCount\":        ghgql.Int(participantsCount),\n\t\t\"commentsCount\":            ghgql.Int(commentsCount),\n\t\t\"commitsCount\":             ghgql.Int(commitsCount),\n\t\t\"statusCheckContextsCount\": ghgql.Int(statusCheckContextsCount),\n\t}\n\terr := ghClient.Query(\"PRTL\", &query, variables)\n\tif err != nil {\n\t\tlog.Printf(\"error: %s\\n\", err)\n\t\treturn prDetails{}, err\n\t}\n\treturn query.RepositoryOwner.Repository.PullRequest, nil\n}\n\nfunc getPRTLData(ghClient *ghapi.GraphQLClient, repoOwner string, repoName string, prNumber int, tlItemsCount int) ([]prTLItem, error) {\n\tvar query prTLQuery\n\n\tvariables := map[string]any{\n\t\t\"repositoryOwner\":    ghgql.String(repoOwner),\n\t\t\"repositoryName\":     ghgql.String(repoName),\n\t\t\"pullRequestNumber\":  ghgql.Int(prNumber),\n\t\t\"timelineItemsCount\": ghgql.Int(tlItemsCount),\n\t}\n\terr := ghClient.Query(\"PRTL\", &query, variables)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn query.RepositoryOwner.Repository.PullRequest.TimelineItems.Nodes, nil\n}\n"
  },
  {
    "path": "ui/initial.go",
    "content": "package ui\n\nimport (\n\t\"charm.land/bubbles/v2/list\"\n\t\"charm.land/lipgloss/v2\"\n\tghapi \"github.com/cli/go-gh/v2/pkg/api\"\n)\n\nconst (\n\tfetchingPRsTitle = \"fetching PRs...\"\n)\n\nfunc InitialModel(ghClient *ghapi.GraphQLClient, config Config, mode Mode) Model {\n\tprListDel := newPRListItemDel()\n\tprTLListDel := newPRTLListItemDel()\n\n\tprDetailsCache := make(map[string]prDetails)\n\tprTLCache := make(map[string][]*prTLItemResult)\n\n\tprDetailsCurSectionCache := make(map[string]uint)\n\n\tm := Model{\n\t\tmode:                     mode,\n\t\tconfig:                   config,\n\t\tghClient:                 ghClient,\n\t\tprsList:                  list.New(nil, prListDel, 0, 0),\n\t\tprTLList:                 list.New(nil, prTLListDel, 0, 0),\n\t\tprDetailsCache:           prDetailsCache,\n\t\tprTLCache:                prTLCache,\n\t\tshowHelp:                 true,\n\t\tterminalDetails:          terminalDetails{width: widthBudgetDefault},\n\t\tprDetailsCurSectionCache: prDetailsCurSectionCache,\n\t}\n\n\tswitch m.mode {\n\tcase RepoMode:\n\t\trepoListItems := make([]list.Item, len(config.Repos))\n\t\tif mode == RepoMode {\n\t\t\tfor i, issue := range config.Repos {\n\t\t\t\trepoListItems[i] = issue\n\t\t\t}\n\t\t}\n\t\trepoListDel := newRepoListItemDel()\n\t\tm.repoList = list.New(repoListItems, repoListDel, 0, 0)\n\t\tm.repoList.Title = \"Repos\"\n\t\tm.repoList.SetStatusBarItemName(\"repo\", \"repos\")\n\t\tm.repoList.DisableQuitKeybindings()\n\t\tm.repoList.SetShowHelp(false)\n\t\tm.repoList.SetFilteringEnabled(false)\n\t\tm.repoList.Styles.Title = m.repoList.Styles.Title.Background(lipgloss.Color(repoListColor)).\n\t\t\tForeground(lipgloss.Color(defaultBackgroundColor)).\n\t\t\tBold(true)\n\t\tm.repoList.KeyMap.PrevPage.SetKeys(\"left\", \"h\", \"pgup\")\n\t\tm.repoList.KeyMap.NextPage.SetKeys(\"right\", \"l\", \"pgdown\")\n\tcase QueryMode:\n\t\tm.activePane = prListView\n\t}\n\n\tm.prsList.Title = fetchingPRsTitle\n\tm.prsList.SetStatusBarItemName(\"PR\", \"PRs\")\n\tm.prsList.DisableQuitKeybindings()\n\tm.prsList.SetShowHelp(false)\n\tm.prsList.SetFilteringEnabled(false)\n\tm.prsList.Styles.Title = m.prsList.Styles.Title.Background(lipgloss.Color(fetchingColor)).\n\t\tForeground(lipgloss.Color(defaultBackgroundColor)).\n\t\tBold(true)\n\tm.prsList.KeyMap.PrevPage.SetKeys(\"left\", \"h\", \"pgup\")\n\tm.prsList.KeyMap.NextPage.SetKeys(\"right\", \"l\", \"pgdown\")\n\n\tm.prTLList.Title = \"fetching timeline...\"\n\tm.prTLList.SetStatusBarItemName(\"item\", \"items\")\n\tm.prTLList.DisableQuitKeybindings()\n\tm.prTLList.SetShowHelp(false)\n\tm.prTLList.SetFilteringEnabled(false)\n\tm.prTLList.Styles.Title = m.prTLList.Styles.Title.Background(lipgloss.Color(prTLListColor)).\n\t\tForeground(lipgloss.Color(defaultBackgroundColor)).\n\t\tBold(true)\n\tm.prTLList.KeyMap.PrevPage.SetKeys(\"left\", \"h\", \"pgup\")\n\tm.prTLList.KeyMap.NextPage.SetKeys(\"right\", \"l\", \"pgdown\")\n\n\treturn m\n}\n"
  },
  {
    "path": "ui/model.go",
    "content": "package ui\n\nimport (\n\t\"time\"\n\n\t\"charm.land/bubbles/v2/list\"\n\t\"charm.land/bubbles/v2/viewport\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"github.com/charmbracelet/glamour\"\n\tghapi \"github.com/cli/go-gh/v2/pkg/api\"\n)\n\ntype Pane uint\n\nconst (\n\trepoListView Pane = iota\n\tprListView\n\tprDetailsView\n\treviewPRListView\n\tprTLListView\n\tprTLItemDetailView\n\thelpView\n)\n\ntype Mode uint\n\nconst (\n\tQueryMode Mode = iota\n\tRepoMode\n)\n\ntype Model struct {\n\tmode                     Mode\n\tconfig                   Config\n\tghClient                 *ghapi.GraphQLClient\n\trepoOwner                string\n\trepoName                 string\n\trepoList                 list.Model\n\tprsList                  list.Model\n\tprTLList                 list.Model\n\tprCache                  []*prResult\n\tprTLItemDetailVP         viewport.Model\n\tprTLItemDetailVPReady    bool\n\tprDetailsTitle           string\n\tprTLItemDetailTitle      string\n\tprDetailsVP              viewport.Model\n\tprDetailsVPReady         bool\n\tprDetailsCache           map[string]prDetails\n\tprTLCache                map[string][]*prTLItemResult\n\tmessage                  string\n\thelpVP                   viewport.Model\n\thelpVPReady              bool\n\tactivePane               Pane\n\tlastPane                 Pane\n\tsecondLastActivePane     Pane\n\tshowHelp                 bool\n\trepoChosen               bool\n\tterminalDetails          terminalDetails\n\tmdRenderer               *glamour.TermRenderer\n\tprDetailsCurrentSection  uint\n\tprDetailsCurSectionCache map[string]uint\n\tprRevCurCmtNum           uint\n}\n\nfunc (m Model) Init() tea.Cmd {\n\tvar cmds []tea.Cmd\n\tcmds = append(cmds, hideHelp(time.Minute*1))\n\n\tif m.mode == QueryMode {\n\t\tcmds = append(cmds, fetchPRSFromQuery(m.ghClient, *m.config.Query, m.config.PRCount))\n\t}\n\n\treturn tea.Batch(cmds...)\n}\n"
  },
  {
    "path": "ui/msgs.go",
    "content": "package ui\n\ntype hideHelpMsg struct{}\n\ntype repoChosenMsg struct {\n\trepo string\n}\n\ntype prsFetchedMsg struct {\n\tprs []pr\n\terr error\n}\n\ntype prMetadataFetchedMsg struct {\n\trepoOwner string\n\trepoName  string\n\tprNumber  int\n\tmetadata  prDetails\n\terr       error\n}\n\ntype reviewPRsFetchedMsg prsFetchedMsg\n\ntype authoredPRsFetchedMsg prsFetchedMsg\n\ntype prTLFetchedMsg struct {\n\trepoOwner string\n\trepoName  string\n\tprNumber  int\n\tprTLItems []prTLItem\n\tsetItems  bool\n\terr       error\n}\n\ntype urlOpenedinBrowserMsg struct {\n\turl string\n\terr error\n}\n\ntype prDiffDoneMsg struct {\n\terr error\n}\n\ntype prViewDoneMsg struct {\n\terr error\n}\n"
  },
  {
    "path": "ui/navigation.go",
    "content": "package ui\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\nconst (\n\tmaxCommentsForNavIndicator = 8\n\tdisabledSectionMarker      = \"◌\"\n\tinactiveSectionMarker      = \"◯\"\n\tactiveSectionMarker        = \"●\"\n)\n\nfunc (m *Model) setPRDetailsContent(prDetails prDetails, section PRDetailSection) {\n\tcontent := fmt.Sprintf(`# %s (%s/%s/pull/%d)\n`, prDetails.PRTitle, prDetails.Repository.Owner.Login, prDetails.Repository.Name, prDetails.Number,\n\t)\n\n\tswitch section {\n\tcase PRMetadata:\n\t\tcontent += prDetails.Metadata()\n\tcase PRDescription:\n\t\tcontent += prDetails.Description()\n\tcase PRChecks:\n\t\tcontent += prDetails.Checks()\n\tcase PRReferences:\n\t\tcontent += prDetails.References()\n\tcase PRFilesChanged:\n\t\tcontent += prDetails.FilesChanged()\n\tcase PRCommits:\n\t\tcontent += prDetails.CommitsList()\n\tcase PRComments:\n\t\tcontent += prDetails.CommentsList()\n\t}\n\n\tglErr := true\n\tif m.mdRenderer != nil {\n\t\tcontentGl, err := m.mdRenderer.Render(content)\n\t\tif err == nil {\n\t\t\tm.prDetailsVP.SetContent(contentGl)\n\t\t\tglErr = false\n\t\t}\n\t}\n\tif glErr {\n\t\tm.prDetailsVP.SetContent(content)\n\t}\n\n\tsections := make([]string, len(PRDetailsSectionList))\n\tfor i := range PRDetailsSectionList {\n\t\tsections[i] = inactiveSectionMarker\n\t}\n\n\tif prDetails.Body == \"\" {\n\t\tsections[PRDescription] = disabledSectionMarker\n\t}\n\t// not foolproof, but should work in most cases\n\t// func (pr prDetails) Checks() will return with an appropriate message in\n\t// that case\n\n\t//nolint:staticcheck\n\tif !(len(prDetails.LastCommit.Nodes) > 0 && prDetails.LastCommit.Nodes[0].Commit.StatusCheckRollup != nil) {\n\t\tsections[PRChecks] = disabledSectionMarker\n\t}\n\tif len(prDetails.IssueReferences.Nodes) == 0 {\n\t\tsections[PRReferences] = disabledSectionMarker\n\t}\n\tif len(prDetails.Files.Nodes) == 0 {\n\t\tsections[PRFilesChanged] = disabledSectionMarker\n\t}\n\tif len(prDetails.Commits.Nodes) == 0 {\n\t\tsections[PRCommits] = disabledSectionMarker\n\t}\n\tif len(prDetails.Comments.Nodes) == 0 {\n\t\tsections[PRComments] = disabledSectionMarker\n\t}\n\n\tsections[section] = activeSectionMarker\n\n\tm.prDetailsTitle = fmt.Sprintf(\"PR Details%s\", \"  \"+strings.Join(sections, \" \"))\n\n\tm.prDetailsVP.GotoTop()\n}\n\nfunc (m *Model) GoToPRDetailSection(section uint) {\n\tif m.prDetailsCurrentSection == section {\n\t\treturn\n\t}\n\tpr, ok := m.prsList.SelectedItem().(*prResult)\n\tif !ok {\n\t\treturn\n\t}\n\n\tprDetails, ok := m.prDetailsCache[fmt.Sprintf(\"%s/%s:%d\", pr.pr.Repository.Owner.Login, pr.pr.Repository.Name, pr.pr.Number)]\n\tif !ok {\n\t\treturn\n\t}\n\tswitch section {\n\tcase 1:\n\t\tif prDetails.Body == \"\" {\n\t\t\treturn\n\t\t}\n\tcase 2:\n\t\t//nolint:staticcheck\n\t\tif !(len(prDetails.LastCommit.Nodes) > 0 && prDetails.LastCommit.Nodes[0].Commit.StatusCheckRollup != nil) {\n\t\t\treturn\n\t\t}\n\tcase 3:\n\t\tif len(prDetails.IssueReferences.Nodes) == 0 {\n\t\t\treturn\n\t\t}\n\tcase 4:\n\t\tif len(prDetails.Files.Nodes) == 0 {\n\t\t\treturn\n\t\t}\n\tcase 5:\n\t\tif len(prDetails.Commits.Nodes) == 0 {\n\t\t\treturn\n\t\t}\n\tcase 6:\n\t\tif len(prDetails.Comments.Nodes) == 0 {\n\t\t\treturn\n\t\t}\n\t}\n\n\tm.setPRDetailsContent(prDetails, PRDetailsSectionList[section])\n\tm.prDetailsCurrentSection = section\n}\n\nfunc (m *Model) setPRReviewCmt(tlItem *prTLItem, commentNum uint) {\n\trevCmts := tlItem.PullRequestReview.Comments.Nodes\n\tvar sectionsStr string\n\n\tif len(revCmts) > maxCommentsForNavIndicator {\n\t\tsectionsStr = fmt.Sprintf(\"%d/%d\", commentNum+1, len(revCmts))\n\t} else if len(revCmts) > 1 {\n\t\tsections := make([]string, len(revCmts))\n\t\tfor i := range revCmts {\n\t\t\tsections[i] = inactiveSectionMarker\n\t\t}\n\t\tsections[commentNum] = activeSectionMarker\n\t\tsectionsStr = \"  \" + strings.Join(sections, \" \")\n\t}\n\n\tvar outdated string\n\tif revCmts[commentNum].Outdated {\n\t\toutdated = \" `(outdated)`\"\n\t}\n\n\tcontent := fmt.Sprintf(\"# from @%s\\n## %s%s\\n%s\\n```diff\\n%s\\n```\", tlItem.PullRequestReview.Author.Login, revCmts[commentNum].Path, outdated, revCmts[commentNum].Body, revCmts[commentNum].DiffHunk)\n\n\tglErr := true\n\tif m.mdRenderer != nil {\n\t\tcontentGl, err := m.mdRenderer.Render(content)\n\t\tif err == nil {\n\t\t\tm.prTLItemDetailVP.SetContent(contentGl)\n\t\t\tglErr = false\n\t\t}\n\t}\n\tif glErr {\n\t\tm.prDetailsVP.SetContent(content)\n\t}\n\n\tm.prTLItemDetailTitle = fmt.Sprintf(\"Review Comments%s\", sectionsStr)\n\tm.prTLItemDetailVP.GotoTop()\n}\n"
  },
  {
    "path": "ui/pr_delegate.go",
    "content": "package ui\n\nimport (\n\t\"charm.land/bubbles/v2/list\"\n\t\"charm.land/lipgloss/v2\"\n)\n\nfunc newPRListItemDel() list.DefaultDelegate {\n\td := list.NewDefaultDelegate()\n\n\td.Styles.SelectedTitle = d.Styles.\n\t\tSelectedTitle.\n\t\tForeground(lipgloss.Color(prListColor)).\n\t\tBorderLeftForeground(lipgloss.Color(prListColor))\n\td.Styles.SelectedDesc = d.Styles.\n\t\tSelectedTitle\n\n\treturn d\n}\n"
  },
  {
    "path": "ui/prtl_delegate.go",
    "content": "package ui\n\nimport (\n\t\"charm.land/bubbles/v2/list\"\n\t\"charm.land/lipgloss/v2\"\n)\n\nfunc newPRTLListItemDel() list.DefaultDelegate {\n\td := list.NewDefaultDelegate()\n\n\td.Styles.SelectedTitle = d.Styles.\n\t\tSelectedTitle.\n\t\tForeground(lipgloss.Color(prTLListColor)).\n\t\tBorderLeftForeground(lipgloss.Color(prTLListColor))\n\td.Styles.SelectedDesc = d.Styles.\n\t\tSelectedTitle\n\n\treturn d\n}\n"
  },
  {
    "path": "ui/render_helpers.go",
    "content": "package ui\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\thumanize \"github.com/dustin/go-humanize\"\n)\n\nconst (\n\twidthBudgetDefault    = 80\n\tresponsiveWidthCutOff = 100\n\twideScreenWidthFrac   = 0.9\n)\n\nfunc getPRTitle(pr *pr) string {\n\tif pr == nil {\n\t\treturn \"\"\n\t}\n\n\tvar reviewDecision string\n\n\tif pr.ReviewDecision != nil {\n\t\tswitch *pr.ReviewDecision {\n\t\tcase \"CHANGES_REQUESTED\":\n\t\t\treviewDecision = \"± \"\n\t\tcase \"APPROVED\":\n\t\t\treviewDecision = \"✅ \"\n\t\tcase \"REVIEW_REQUIRED\":\n\t\t\treviewDecision = \"🟡 \"\n\t\t}\n\t}\n\treturn fmt.Sprintf(\"%s#%2d %s\", reviewDecision, pr.Number, pr.PRTitle)\n}\n\nfunc getPRDesc(pr *pr, mode Mode, terminalDetails terminalDetails) string {\n\tif pr == nil {\n\t\treturn \"\"\n\t}\n\n\tvar widthBudget int\n\tvar widthFixed int\n\tvar additions string\n\tvar deletions string\n\tvar reviews string\n\tvar desc string\n\n\tswitch mode {\n\tcase RepoMode:\n\t\twidthFixed = 30\n\tcase QueryMode:\n\t\twidthFixed = 16\n\t}\n\n\tif terminalDetails.width > responsiveWidthCutOff {\n\t\twidthBudget = getFracInt(terminalDetails.width, wideScreenWidthFrac) - widthFixed\n\t} else {\n\t\twidthBudget = terminalDetails.width - widthFixed\n\t}\n\n\tif widthBudget < 0 {\n\t\twidthBudget = widthBudgetDefault\n\t}\n\n\tif pr.Additions > 0 {\n\t\tadditions = additionsStyle.Render(fmt.Sprintf(\"+%d\", pr.Additions))\n\t}\n\tif pr.Deletions > 0 {\n\t\tdeletions = deletionsStyle.Render(fmt.Sprintf(\"-%d\", pr.Deletions))\n\t}\n\n\tif pr.Reviews.TotalCount > 0 {\n\t\treviews = numReviewsStyle.Render(fmt.Sprintf(\"%dr\", pr.Reviews.TotalCount))\n\t}\n\n\tswitch mode {\n\tcase RepoMode:\n\t\tupdatedAt := dateStyle.Render(RightPadTrim(\"updated \"+humanize.Time(pr.UpdatedAt), getFracInt(widthBudget, 0.3)))\n\t\tauthor := getDynamicStyle(pr.Author.Login).Render(RightPadTrim(pr.Author.Login, getFracInt(widthBudget, 0.7)))\n\t\tstate := prStyle(pr.State).Render(pr.State)\n\n\t\tdesc = fmt.Sprintf(\"%s%s%s%s%s%s\", author, updatedAt, state, additions, deletions, reviews)\n\n\tcase QueryMode:\n\t\trepoStr := fmt.Sprintf(\"%s/%s\", pr.Repository.Owner.Login, pr.Repository.Name)\n\t\tupdatedAt := dateStyle.Render(RightPadTrim(\"updated \"+humanize.Time(pr.UpdatedAt), getFracInt(widthBudget, 0.3)))\n\t\tauthor := getDynamicStyle(pr.Author.Login).Render(RightPadTrim(pr.Author.Login, getFracInt(widthBudget, 0.4)))\n\t\tstate := prStyle(pr.State).Render(pr.State)\n\t\trepo := getDynamicStyle(repoStr).Render(RightPadTrim(repoStr, getFracInt(widthBudget, 0.3)))\n\n\t\tdesc = fmt.Sprintf(\"%s%s%s%s%s%s%s\", author, repo, updatedAt, state, additions, deletions, reviews)\n\t}\n\n\treturn desc\n}\n\nfunc getPRTLItemTitle(item *prTLItem) string {\n\tvar title string\n\tvar date string\n\n\tswitch item.Type {\n\tcase tlItemPRCommit:\n\t\tif item.PullRequestCommit.Commit.Author.User != nil {\n\t\t\tauthor := getDynamicStyle(item.PullRequestCommit.Commit.Author.User.Login).Render(item.PullRequestCommit.Commit.Author.User.Login)\n\t\t\tdate = dateStyle.Render(humanize.Time(item.PullRequestCommit.Commit.CommittedDate))\n\t\t\ttitle = fmt.Sprintf(\"%spushed a commit%s\", author, date)\n\t\t} else {\n\t\t\ttitle = fmt.Sprintf(\"%s pushed a commit\", item.PullRequestCommit.Commit.Author.Name)\n\t\t}\n\n\tcase tlItemHeadRefForcePushed:\n\t\tactor := getDynamicStyle(item.HeadRefForcePushed.Actor.Login).Render(item.HeadRefForcePushed.Actor.Login)\n\t\tbeforeCommitHash := item.HeadRefForcePushed.BeforeCommit.AbbreviatedOid\n\t\tafterCommitHash := item.HeadRefForcePushed.AfterCommit.AbbreviatedOid\n\t\tdate = dateStyle.Render(humanize.Time(item.HeadRefForcePushed.CreatedAt))\n\t\ttitle = fmt.Sprintf(\"%sforce pushed head ref from %s to %s%s\", actor, beforeCommitHash, afterCommitHash, date)\n\n\tcase tlItemPRReadyForReview:\n\t\tactor := getDynamicStyle(item.PullRequestReadyForReview.Actor.Login).Render(item.PullRequestReadyForReview.Actor.Login)\n\t\ttitle = fmt.Sprintf(\"%smarked PR as ready for review\", actor)\n\n\tcase tlItemPRReviewRequested:\n\t\tactor := getDynamicStyle(item.PullRequestReviewRequested.Actor.Login).Render(item.PullRequestReviewRequested.Actor.Login)\n\t\treviewer := getDynamicStyle(item.PullRequestReviewRequested.RequestedReviewer.User.Login).Render(item.PullRequestReviewRequested.RequestedReviewer.User.Login)\n\t\ttitle = fmt.Sprintf(\"%srequested a review from %s\", actor, reviewer)\n\n\tcase tlItemPRReview:\n\t\tauthor := getDynamicStyle(item.PullRequestReview.Author.Login).Render(item.PullRequestReview.Author.Login)\n\t\tdate = dateStyle.Render(humanize.Time(item.PullRequestReview.CreatedAt))\n\t\tvar comments string\n\t\tvar more string\n\t\tif item.PullRequestReview.Comments.TotalCount > 0 {\n\t\t\tmore = \" ⏎\"\n\t\t}\n\t\tif item.PullRequestReview.Comments.TotalCount > 1 {\n\t\t\tcomments = numCommentsStyle.Render(fmt.Sprintf(\"with %d comments\", item.PullRequestReview.Comments.TotalCount))\n\t\t} else if item.PullRequestReview.Comments.TotalCount == 1 {\n\t\t\tcomments = numCommentsStyle.Render(\"with 1 comment\")\n\t\t}\n\t\ttitle = fmt.Sprintf(\"%sreviewed%s%s%s\", author, comments, date, more)\n\n\tcase tlItemMergedEvent:\n\t\tauthor := getDynamicStyle(item.MergedEvent.Actor.Login).Render(item.MergedEvent.Actor.Login)\n\t\tdate = dateStyle.Render(humanize.Time(item.MergedEvent.CreatedAt))\n\t\ttitle = fmt.Sprintf(\"%smerged the PR%s\", author, date)\n\t}\n\treturn title\n}\n\nfunc getPRTLItemDesc(item *prTLItem) string {\n\tvar desc string\n\tswitch item.Type {\n\tcase tlItemPRCommit:\n\t\tdesc = fmt.Sprintf(\"📧 %s\", item.PullRequestCommit.Commit.MessageHeadline)\n\tcase tlItemHeadRefForcePushed:\n\t\tdesc = fmt.Sprintf(\"💪 %s\", item.HeadRefForcePushed.AfterCommit.MessageHeadline)\n\tcase tlItemPRReadyForReview:\n\t\tdesc = fmt.Sprintf(\"🚦%s\", dateStyle.Render(humanize.Time(item.PullRequestReadyForReview.CreatedAt)))\n\tcase tlItemPRReviewRequested:\n\t\tdesc = fmt.Sprintf(\"🙏%s\", dateStyle.Render(humanize.Time(item.PullRequestReviewRequested.CreatedAt)))\n\tcase tlItemPRReview:\n\t\treviewState := reviewStyle(item.PullRequestReview.State).Render(item.PullRequestReview.State)\n\t\tvar comment string\n\t\tif item.PullRequestReview.Body != \"\" {\n\t\t\tcomment = fmt.Sprintf(\"with comment: %s\", strings.Split(item.PullRequestReview.Body, \"\\r\")[0])\n\t\t}\n\t\tdesc = fmt.Sprintf(\"🔎 %s%s\", reviewState, comment)\n\tcase tlItemMergedEvent:\n\t\tdesc = fmt.Sprintf(\"🚀 message: %s\", item.MergedEvent.MergeCommit.MessageHeadline)\n\t}\n\treturn desc\n}\n"
  },
  {
    "path": "ui/repo_delegate.go",
    "content": "package ui\n\nimport (\n\t\"charm.land/bubbles/v2/list\"\n\t\"charm.land/lipgloss/v2\"\n)\n\nfunc newRepoListItemDel() list.DefaultDelegate {\n\td := list.NewDefaultDelegate()\n\n\td.Styles.SelectedTitle = d.Styles.\n\t\tSelectedTitle.\n\t\tForeground(lipgloss.Color(repoListColor)).\n\t\tBorderLeftForeground(lipgloss.Color(repoListColor))\n\td.Styles.SelectedDesc = d.Styles.\n\t\tSelectedTitle\n\n\treturn d\n}\n"
  },
  {
    "path": "ui/styles.go",
    "content": "package ui\n\nimport (\n\t\"hash/fnv\"\n\n\t\"charm.land/lipgloss/v2\"\n)\n\nconst (\n\tdefaultBackgroundColor      = \"#282828\"\n\trepoListColor               = \"#b8bb26\"\n\tprListColor                 = \"#fe8019\"\n\tprTLListColor               = \"#d3869b\"\n\tprDetailsTitleColor         = \"#fabd2f\"\n\trevCmtListColor             = \"#8ec07c\"\n\tprOpenColor                 = \"#fabd2f\"\n\tprMergedColor               = \"#b8bb26\"\n\tprClosedColor               = \"#928374\"\n\tadditionsColor              = \"#8ec07c\"\n\tdeletionsColor              = \"#fb4934\"\n\treviewCommentedColor        = \"#83a598\"\n\treviewApprovedColor         = \"#b8bb26\"\n\treviewChangesRequestedColor = \"#fabd2f\"\n\treviewDismissedColor        = \"#928374\"\n\tdateColor                   = \"#928374\"\n\trepoColor                   = \"#bdae93\"\n\tnumReviewsColor             = \"#665c54\"\n\tnumCommentsColor            = \"#83a598\"\n\tfooterColor                 = \"#7c6f64\"\n\thelpMsgColor                = \"#83a598\"\n\thelpViewTitleColor          = \"#83a598\"\n\ttoolNameColor               = \"#b8bb26\"\n\tfetchingColor               = \"#928374\"\n)\n\nfunc getDynamicStyle(author string) lipgloss.Style {\n\th := fnv.New32()\n\th.Write([]byte(author))\n\thash := h.Sum32()\n\n\tcolor := colors[int(hash)%len(colors)]\n\n\tst := lipgloss.NewStyle().\n\t\tPaddingRight(1).\n\t\tForeground(lipgloss.Color(color))\n\n\treturn st\n}\n\nvar (\n\tbaseStyle = lipgloss.NewStyle().\n\t\t\tForeground(lipgloss.Color(defaultBackgroundColor))\n\n\ttoolNameStyle = baseStyle.\n\t\t\tAlign(lipgloss.Center).\n\t\t\tPaddingLeft(1).\n\t\t\tPaddingRight(1).\n\t\t\tBold(true).\n\t\t\tBackground(lipgloss.Color(toolNameColor))\n\n\tlistStyle = baseStyle.\n\t\t\tPaddingTop(1).\n\t\t\tPaddingBottom(1).\n\t\t\tForeground(lipgloss.Color(defaultBackgroundColor))\n\n\tviewPortStyle = lipgloss.NewStyle().\n\t\t\tPaddingTop(1).\n\t\t\tPaddingBottom(1)\n\n\thelpMsgStyle = baseStyle.\n\t\t\tPaddingLeft(2).\n\t\t\tBold(true).\n\t\t\tForeground(lipgloss.Color(helpMsgColor))\n\n\tdateStyle = lipgloss.NewStyle().\n\t\t\tPaddingLeft(1).\n\t\t\tForeground(lipgloss.Color(dateColor))\n\n\tnumReviewsStyle = lipgloss.NewStyle().\n\t\t\tPaddingLeft(1).\n\t\t\tForeground(lipgloss.Color(numReviewsColor))\n\n\tnumCommentsStyle = lipgloss.NewStyle().\n\t\t\t\tPaddingLeft(1).\n\t\t\t\tForeground(lipgloss.Color(numCommentsColor))\n\n\tlinesChangedStyle = lipgloss.NewStyle().\n\t\t\t\tPaddingLeft(1)\n\n\tadditionsStyle = linesChangedStyle.\n\t\t\tPaddingLeft(2).\n\t\t\tForeground(lipgloss.Color(additionsColor))\n\n\tdeletionsStyle = linesChangedStyle.\n\t\t\tForeground(lipgloss.Color(deletionsColor))\n\n\tprStyle = func(state string) lipgloss.Style {\n\t\tst := lipgloss.NewStyle().\n\t\t\tPaddingLeft(1).\n\t\t\tPaddingRight(1).\n\t\t\tWidth(8).\n\t\t\tBold(true).\n\t\t\tAlign(lipgloss.Center).\n\t\t\tForeground(lipgloss.Color(defaultBackgroundColor))\n\n\t\tvar bgColor string\n\t\tswitch state {\n\t\tcase prStateOpen:\n\t\t\tbgColor = prOpenColor\n\t\tcase prStateMerged:\n\t\t\tbgColor = prMergedColor\n\t\tdefault:\n\t\t\tbgColor = prClosedColor\n\t\t}\n\t\treturn st.Background(lipgloss.Color(bgColor))\n\t}\n\n\treviewStyle = func(state string) lipgloss.Style {\n\t\tst := lipgloss.NewStyle().\n\t\t\tPaddingRight(1).\n\t\t\tBold(true).\n\t\t\tAlign(lipgloss.Center)\n\n\t\tvar bgColor string\n\t\tswitch state {\n\t\tcase reviewCommented:\n\t\t\tbgColor = reviewCommentedColor\n\t\tcase reviewApproved:\n\t\t\tbgColor = reviewApprovedColor\n\t\tcase reviewChangesRequested:\n\t\t\tbgColor = reviewChangesRequestedColor\n\t\tdefault:\n\t\t\tbgColor = reviewDismissedColor\n\t\t}\n\t\treturn st.Foreground(lipgloss.Color(bgColor))\n\t}\n\n\tfooterStyle = lipgloss.NewStyle().\n\t\t\tForeground(lipgloss.Color(defaultBackgroundColor)).\n\t\t\tBackground(lipgloss.Color(footerColor))\n\n\ttitleStyle = lipgloss.NewStyle().\n\t\t\tPaddingLeft(1).\n\t\t\tPaddingRight(1).\n\t\t\tBold(true).\n\t\t\tForeground(lipgloss.Color(defaultBackgroundColor))\n\n\thelpVPTitleStyle = titleStyle.\n\t\t\t\tBackground(lipgloss.Color(helpViewTitleColor))\n\n\tprDetailsTitleStyle = titleStyle.\n\t\t\t\tBackground(lipgloss.Color(prDetailsTitleColor))\n)\n"
  },
  {
    "path": "ui/types.go",
    "content": "package ui\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/dustin/go-humanize\"\n)\n\nconst (\n\tprStateOpen                 = \"OPEN\"\n\tprStateMerged               = \"MERGED\"\n\tprStateClosed               = \"CLOSED\"\n\tprRevDecChangesReq          = \"CHANGES_REQUESTED\"\n\tprRevDecApproved            = \"APPROVED\"\n\tprRevDecRevReq              = \"REVIEW_REQUIRED\"\n\ttlItemPRCommit              = \"PullRequestCommit\"\n\ttlItemPRReadyForReview      = \"ReadyForReviewEvent\"\n\ttlItemPRReviewRequested     = \"ReviewRequestedEvent\"\n\ttlItemPRReview              = \"PullRequestReview\"\n\ttlItemMergedEvent           = \"MergedEvent\"\n\ttlItemHeadRefForcePushed    = \"HeadRefForcePushedEvent\"\n\treviewPending               = \"PENDING\"\n\treviewCommented             = \"COMMENTED\"\n\treviewApproved              = \"APPROVED\"\n\treviewChangesRequested      = \"CHANGES_REQUESTED\"\n\treviewDismissed             = \"DISMISSED\"\n\tcheckStatusStateCompleted   = \"COMPLETED\"\n\tcheckRunType                = \"CheckRun\"\n\tstatusContextType           = \"StatusContext\"\n\tcheckConclusionStateSuccess = \"SUCCESS\"\n\tcheckConclusionStateFailure = \"FAILURE\"\n\tcheckConclusionStateError   = \"ERROR\"\n\tstatusStateSuccess          = \"SUCCESS\"\n\tstatusStateFailure          = \"FAILURE\"\n\tstatusStateError            = \"ERROR\"\n\trequestedReviewerUser       = \"User\"\n\tprDetailsMetadataKeyPadding = 30\n\tcheckNamePadding            = 40\n\tstatusConclusionPadding     = 16\n\treviewRequestsCount         = 20\n\tlatestReviewsCount          = 30\n\tfilesCount                  = 50\n\tlabelsCount                 = 10\n\tassigneesCount              = 10\n\tissuesCount                 = 10\n\tparticipantsCount           = 30\n\tcommentsCount               = 10\n\tcommitsCount                = 30\n\tstatusCheckContextsCount    = 50\n\ttimeFormat                  = \"2006/01/02 15:04\"\n\tmergeableConflicting        = \"CONFLICTING\"\n\tnoChecksHeader              = \"## No Checks\"\n)\n\ntype terminalDetails struct {\n\twidth int\n}\n\ntype SourceConfig struct {\n\tDiffPager *string `yaml:\"diff-pager\"`\n\tPRCount   *int    `yaml:\"pr-count\"`\n\tSources   *[]struct {\n\t\tOwner string `yaml:\"owner\"`\n\t\tRepos []struct {\n\t\t\tName string `yaml:\"name\"`\n\t\t} `yaml:\"repos\"`\n\t} `yaml:\"sources\"`\n\tQuery *string `yaml:\"query\"`\n}\n\ntype Repo struct {\n\tOwner string\n\tName  string\n}\n\ntype Config struct {\n\tPRCount int\n\tRepos   []Repo\n\tQuery   *string\n}\n\ntype prResult struct {\n\tpr          *pr\n\ttitle       string\n\tdescription string\n\tidentifier  string\n}\n\ntype prTLItemResult struct {\n\titem        *prTLItem\n\ttitle       string\n\tdescription string\n}\n\ntype pr struct {\n\tNumber     int\n\tPRTitle    string `graphql:\"prTitle: title\"`\n\tRepository struct {\n\t\tOwner struct {\n\t\t\tLogin string\n\t\t}\n\t\tName string\n\t}\n\tState          string\n\tMergeable      string\n\tIsDraft        bool\n\tReviewDecision *string\n\tCreatedAt      time.Time\n\tUpdatedAt      time.Time\n\tClosedAt       *time.Time\n\tMergedAt       *time.Time\n\tLastEditedAt   *time.Time\n\tAuthor         struct {\n\t\tLogin string\n\t}\n\tURL       string\n\tAdditions int\n\tDeletions int\n\tReviews   struct {\n\t\tTotalCount int\n\t}\n}\n\ntype prDetails struct {\n\tNumber     int\n\tPRTitle    string `graphql:\"prTitle: title\"`\n\tRepository struct {\n\t\tOwner struct {\n\t\t\tLogin string\n\t\t}\n\t\tName string\n\t}\n\tState          string\n\tMergeable      string\n\tIsDraft        bool\n\tReviewDecision *string\n\tCreatedAt      time.Time\n\tUpdatedAt      time.Time\n\tClosedAt       *time.Time\n\tMergedAt       *time.Time\n\tLastEditedAt   *time.Time\n\tAuthor         struct {\n\t\tLogin string\n\t}\n\tAdditions      int\n\tDeletions      int\n\tReviewRequests *struct {\n\t\tNodes []struct {\n\t\t\tRequestedReviewer *struct {\n\t\t\t\tType string `graphql:\"type: __typename\"`\n\t\t\t\tUser struct {\n\t\t\t\t\tLogin string\n\t\t\t\t} `graphql:\"... on User \"`\n\t\t\t}\n\t\t}\n\t} `graphql:\"reviewRequests (first:$reviewRequestsCount)\"`\n\tLatestReviews struct {\n\t\tNodes []struct {\n\t\t\tAuthor struct {\n\t\t\t\tLogin string\n\t\t\t}\n\t\t\tState string\n\t\t}\n\t} `graphql:\"latestReviews (last: $latestReviewsCount)\"`\n\tBody  string\n\tFiles struct {\n\t\tNodes []struct {\n\t\t\tPath      string\n\t\t\tAdditions int\n\t\t\tDeletions int\n\t\t}\n\t} `graphql:\"files (first: $filesCount)\"`\n\tLabels struct {\n\t\tNodes []struct {\n\t\t\tName string\n\t\t}\n\t} `graphql:\"labels (first: $labelsCount)\"`\n\tAssignees struct {\n\t\tNodes []struct {\n\t\t\tLogin string\n\t\t}\n\t} `graphql:\"assignees (first: $assigneesCount)\"`\n\tIssueReferences struct {\n\t\tNodes []struct {\n\t\t\tNumber int\n\t\t\tTitle  string\n\t\t\tURL    string\n\t\t}\n\t} `graphql:\"closingIssuesReferences (first: $issuesCount)\"`\n\tParticipants struct {\n\t\tNodes []struct {\n\t\t\tLogin string\n\t\t}\n\t} `graphql:\"participants (first: $participantsCount)\"`\n\tComments struct {\n\t\tTotalCount int\n\t\tNodes      []struct {\n\t\t\tBody      string\n\t\t\tUpdatedAt time.Time\n\t\t\tAuthor    struct {\n\t\t\t\tLogin string\n\t\t\t}\n\t\t}\n\t} `graphql:\"comments (first: $commentsCount)\"`\n\tCommits struct {\n\t\tTotalCount int\n\t\tNodes      []struct {\n\t\t\tCommit struct {\n\t\t\t\tAbbreviatedOid  string\n\t\t\t\tMessageHeadline string\n\t\t\t\tAuthoredDate    time.Time\n\t\t\t\tAuthor          struct {\n\t\t\t\t\tName string\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} `graphql:\"commits (last: $commitsCount)\"`\n\tMergedBy *struct {\n\t\tLogin string\n\t}\n\tMilestone *struct {\n\t\tTitle string\n\t}\n\tLastCommit struct {\n\t\tNodes []struct {\n\t\t\tCommit struct {\n\t\t\t\tAbbreviatedOid    string\n\t\t\t\tStatusCheckRollup *struct {\n\t\t\t\t\tContexts struct {\n\t\t\t\t\t\tNodes []struct {\n\t\t\t\t\t\t\tType     string `graphql:\"type: __typename\"`\n\t\t\t\t\t\t\tCheckRun struct {\n\t\t\t\t\t\t\t\tStatus     string\n\t\t\t\t\t\t\t\tConclusion *string\n\t\t\t\t\t\t\t\tName       string\n\t\t\t\t\t\t\t} `graphql:\"... on CheckRun\"`\n\t\t\t\t\t\t\tStatusContext struct {\n\t\t\t\t\t\t\t\tState   string\n\t\t\t\t\t\t\t\tContext string\n\t\t\t\t\t\t\t} `graphql:\"... on StatusContext\"`\n\t\t\t\t\t\t}\n\t\t\t\t\t} `graphql:\"contexts (first: $statusCheckContextsCount) \"`\n\t\t\t\t\tState string\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} `graphql:\"lastCommit: commits(last: 1)\"`\n}\n\ntype PRDetailSection uint\n\nconst (\n\tPRMetadata PRDetailSection = iota\n\tPRDescription\n\tPRChecks\n\tPRReferences\n\tPRFilesChanged\n\tPRCommits\n\tPRComments\n)\n\nvar PRDetailsSectionList = []PRDetailSection{\n\tPRMetadata,\n\tPRDescription,\n\tPRChecks,\n\tPRReferences,\n\tPRFilesChanged,\n\tPRCommits,\n\tPRComments,\n}\n\ntype prReviewComment struct {\n\tCreatedAt time.Time\n\tBody      string\n\tOutdated  bool\n\tDiffHunk  string\n\tPath      string\n\tURL       string\n}\n\ntype prSearchQuery struct {\n\tSearch struct {\n\t\tEdges []struct {\n\t\t\tNode struct {\n\t\t\t\tType string `graphql:\"type: __typename\"`\n\t\t\t\tpr   `graphql:\"... on PullRequest\"`\n\t\t\t}\n\t\t}\n\t} `graphql:\"search(query: $query, type: ISSUE, first: $count)\"`\n}\n\ntype prDetailsQuery struct {\n\tRepositoryOwner struct {\n\t\tRepository struct {\n\t\t\tPullRequest prDetails `graphql:\"pullRequest(number: $pullRequestNumber)\"`\n\t\t} `graphql:\"repository(name: $repositoryName)\"`\n\t} `graphql:\"repositoryOwner(login: $repositoryOwner)\"`\n}\n\ntype prTLItem struct {\n\tType              string `graphql:\"type: __typename\"`\n\tPullRequestCommit struct {\n\t\tURL    string\n\t\tCommit struct {\n\t\t\tCommittedDate   time.Time\n\t\t\tMessageHeadline string\n\t\t\tAuthor          struct {\n\t\t\t\tName string\n\t\t\t\tUser *struct {\n\t\t\t\t\tLogin string\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} `graphql:\"... on PullRequestCommit\"`\n\tHeadRefForcePushed struct {\n\t\tCreatedAt time.Time\n\t\tActor     struct {\n\t\t\tLogin string\n\t\t}\n\t\tBeforeCommit struct {\n\t\t\tAbbreviatedOid string\n\t\t}\n\t\tAfterCommit struct {\n\t\t\tAbbreviatedOid  string\n\t\t\tURL             string\n\t\t\tMessageHeadline string\n\t\t}\n\t} `graphql:\"... on HeadRefForcePushedEvent\"`\n\tPullRequestReadyForReview struct {\n\t\tCreatedAt time.Time\n\t\tActor     struct {\n\t\t\tLogin string\n\t\t}\n\t} `graphql:\"... on ReadyForReviewEvent\"`\n\tPullRequestReviewRequested struct {\n\t\tCreatedAt time.Time\n\t\tActor     struct {\n\t\t\tLogin string\n\t\t}\n\t\tRequestedReviewer struct {\n\t\t\tUser struct {\n\t\t\t\tLogin string\n\t\t\t} `graphql:\"... on User\"`\n\t\t}\n\t} `graphql:\"... on ReviewRequestedEvent\"`\n\tPullRequestReview struct {\n\t\tURL       string\n\t\tCreatedAt time.Time\n\t\tState     string\n\t\tBody      string\n\t\tComments  struct {\n\t\t\tTotalCount int\n\t\t\tNodes      []prReviewComment\n\t\t} `graphql:\"comments(first: 100)\"`\n\t\tAuthor struct {\n\t\t\tLogin string\n\t\t}\n\t} `graphql:\"... on PullRequestReview\"`\n\tMergedEvent struct {\n\t\tCreatedAt   time.Time\n\t\tURL         string\n\t\tMergeCommit struct {\n\t\t\tMessageHeadline string\n\t\t} `graphql:\"mergeCommit: commit\"`\n\t\tActor struct {\n\t\t\tLogin string\n\t\t}\n\t} `graphql:\"... on MergedEvent\"`\n}\n\ntype prTLQuery struct {\n\tRepositoryOwner struct {\n\t\tRepository struct {\n\t\t\tPullRequest struct {\n\t\t\t\tTimelineItems struct {\n\t\t\t\t\tNodes []prTLItem\n\t\t\t\t} `graphql:\"timelineItems(last: $timelineItemsCount, itemTypes: [PULL_REQUEST_COMMIT, READY_FOR_REVIEW_EVENT, REVIEW_REQUESTED_EVENT, MERGED_EVENT, PULL_REQUEST_REVIEW, HEAD_REF_FORCE_PUSHED_EVENT])\"`\n\t\t\t} `graphql:\"pullRequest(number: $pullRequestNumber)\"`\n\t\t} `graphql:\"repository(name: $repositoryName)\"`\n\t} `graphql:\"repositoryOwner(login: $repositoryOwner)\"`\n}\n\nfunc (pr prDetails) Metadata() string {\n\tvar metadata []string\n\n\tmetadata = append(metadata, fmt.Sprintf(\"- %s *%s*\",\n\t\tRightPadTrim(\"State\", prDetailsMetadataKeyPadding),\n\t\tpr.State,\n\t))\n\n\tmetadata = append(metadata, fmt.Sprintf(\"- %s `@%s`\",\n\t\tRightPadTrim(\"Author\", prDetailsMetadataKeyPadding),\n\t\tpr.Author.Login,\n\t))\n\n\tif len(pr.Assignees.Nodes) > 0 {\n\t\tassignees := make([]string, len(pr.Assignees.Nodes))\n\t\tfor i, l := range pr.Assignees.Nodes {\n\t\t\tassignees[i] = fmt.Sprintf(\"`@%s`\", l.Login)\n\t\t}\n\t\tmetadata = append(metadata, fmt.Sprintf(\"- %s %s\",\n\t\t\tRightPadTrim(\"Assignees\", prDetailsMetadataKeyPadding),\n\t\t\tstrings.Join(assignees, \", \"),\n\t\t))\n\t}\n\n\tif len(pr.Participants.Nodes) > 0 {\n\t\tparticipants := make([]string, len(pr.Participants.Nodes))\n\t\tfor i, l := range pr.Participants.Nodes {\n\t\t\tparticipants[i] = fmt.Sprintf(\"`@%s`\", l.Login)\n\t\t}\n\t\tmetadata = append(metadata, fmt.Sprintf(\"- %s %s\",\n\t\t\tRightPadTrim(\"Participants\", prDetailsMetadataKeyPadding),\n\t\t\tstrings.Join(participants, \", \"),\n\t\t))\n\t}\n\n\tif pr.ReviewRequests != nil && len(pr.ReviewRequests.Nodes) > 0 {\n\t\tvar requested []string\n\t\tfor _, r := range pr.ReviewRequests.Nodes {\n\t\t\tif r.RequestedReviewer.Type != requestedReviewerUser {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trequested = append(requested, fmt.Sprintf(\"`@%s`\", r.RequestedReviewer.User.Login))\n\t\t}\n\n\t\tif len(requested) > 0 {\n\t\t\tmetadata = append(metadata, fmt.Sprintf(\"- %s %s\",\n\t\t\t\tRightPadTrim(\"Review requested from\", prDetailsMetadataKeyPadding),\n\t\t\t\tstrings.Join(requested, \", \"),\n\t\t\t))\n\t\t}\n\t}\n\n\tmetadata = append(metadata, fmt.Sprintf(\"- %s %s (%s)\",\n\t\tRightPadTrim(\"Created at\", prDetailsMetadataKeyPadding),\n\t\tpr.CreatedAt.Format(timeFormat),\n\t\thumanize.Time(pr.CreatedAt),\n\t))\n\tif pr.LastEditedAt != nil && *pr.LastEditedAt != pr.CreatedAt {\n\t\tmetadata = append(metadata, fmt.Sprintf(\"- %s %s (%s)\",\n\t\t\tRightPadTrim(\"Last edited at\", prDetailsMetadataKeyPadding),\n\t\t\tpr.LastEditedAt.Format(timeFormat),\n\t\t\thumanize.Time(*pr.LastEditedAt),\n\t\t))\n\t}\n\n\tswitch pr.State {\n\tcase prStateClosed:\n\t\tif pr.ClosedAt != nil {\n\t\t\tmetadata = append(metadata, fmt.Sprintf(\"- %s %s (%s)\",\n\t\t\t\tRightPadTrim(\"Closed at\", prDetailsMetadataKeyPadding),\n\t\t\t\tpr.ClosedAt.Format(timeFormat),\n\t\t\t\thumanize.Time(*pr.ClosedAt),\n\t\t\t))\n\t\t}\n\tcase prStateMerged:\n\t\tmetadata = append(metadata, fmt.Sprintf(\"- %s %s (%s) by `@%s`\",\n\t\t\tRightPadTrim(\"Merged at\", prDetailsMetadataKeyPadding),\n\t\t\tpr.MergedAt.Format(timeFormat),\n\t\t\thumanize.Time(*pr.MergedAt),\n\t\t\tpr.MergedBy.Login,\n\t\t))\n\t}\n\n\tif len(pr.Labels.Nodes) > 0 {\n\t\tlabels := make([]string, len(pr.Labels.Nodes))\n\t\tfor i, l := range pr.Labels.Nodes {\n\t\t\tlabels[i] = fmt.Sprintf(\"*%s*\", l.Name)\n\t\t}\n\t\tmetadata = append(metadata, fmt.Sprintf(\"- %s %s\",\n\t\t\tRightPadTrim(\"Labels\", prDetailsMetadataKeyPadding),\n\t\t\tstrings.Join(labels, \" \"),\n\t\t))\n\t}\n\n\tif pr.Commits.TotalCount > 0 {\n\t\tmetadata = append(metadata, fmt.Sprintf(\"- %s %d\",\n\t\t\tRightPadTrim(\"Commits\", prDetailsMetadataKeyPadding),\n\t\t\tpr.Commits.TotalCount,\n\t\t))\n\t}\n\n\tif pr.Comments.TotalCount > 0 {\n\t\tmetadata = append(metadata, fmt.Sprintf(\"- %s %d\",\n\t\t\tRightPadTrim(\"Comments\", prDetailsMetadataKeyPadding),\n\t\t\tpr.Comments.TotalCount,\n\t\t))\n\t}\n\n\tif pr.IsDraft {\n\t\tmetadata = append(metadata, fmt.Sprintf(\"- %s `true`\",\n\t\t\tRightPadTrim(\"Is draft\",\n\t\t\t\tprDetailsMetadataKeyPadding),\n\t\t))\n\t}\n\n\tif pr.Mergeable == mergeableConflicting {\n\t\tmetadata = append(metadata, fmt.Sprintf(\"- %s `true`\", RightPadTrim(\"Has conflicts\",\n\t\t\tprDetailsMetadataKeyPadding),\n\t\t))\n\t}\n\n\tif pr.Milestone != nil {\n\t\tmetadata = append(metadata, fmt.Sprintf(\"- %s %s\", RightPadTrim(\"Milestone\",\n\t\t\tprDetailsMetadataKeyPadding),\n\t\t\tpr.Milestone.Title,\n\t\t))\n\t}\n\n\tif len(pr.LatestReviews.Nodes) > 0 {\n\t\treviews := make([]string, len(pr.LatestReviews.Nodes))\n\n\t\tfor i, r := range pr.LatestReviews.Nodes {\n\t\t\tvar state string\n\t\t\tswitch r.State {\n\t\t\tcase reviewPending:\n\t\t\t\tstate = \"🟡\"\n\t\t\tcase reviewCommented:\n\t\t\t\tstate = \"💬\"\n\t\t\tcase reviewChangesRequested:\n\t\t\t\tstate = \"🔄\"\n\t\t\tcase reviewApproved:\n\t\t\t\tstate = \"✅\"\n\t\t\tcase reviewDismissed:\n\t\t\t\tstate = \"❌\"\n\t\t\t}\n\t\t\treviews[i] = fmt.Sprintf(\"`@%s` %s\", r.Author.Login, state)\n\t\t}\n\n\t\tmetadata = append(metadata, \"\\n---\\n\")\n\n\t\tmetadata = append(metadata, fmt.Sprintf(\"- %s %s\",\n\t\t\tRightPadTrim(\"Reviewed by\", prDetailsMetadataKeyPadding),\n\t\t\tstrings.Join(reviews, \", \"),\n\t\t))\n\t}\n\n\treturn fmt.Sprintf(`\n## Metadata\n\n%s`, strings.Join(metadata, \"\\n\"))\n}\n\nfunc (pr prDetails) Description() string {\n\treturn fmt.Sprintf(`\n## Description\n\n%s`, pr.Body)\n}\n\nfunc (pr prDetails) Checks() string {\n\tif len(pr.LastCommit.Nodes) == 0 {\n\t\treturn noChecksHeader\n\t}\n\tif pr.LastCommit.Nodes[0].Commit.StatusCheckRollup == nil {\n\t\treturn noChecksHeader\n\t}\n\tif len(pr.LastCommit.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes) == 0 {\n\t\treturn noChecksHeader\n\t}\n\n\tvar checks []string\n\tfor _, n := range pr.LastCommit.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes {\n\t\tswitch n.Type {\n\t\tcase checkRunType:\n\t\t\tcheckName := RightPadTrim(n.CheckRun.Name, checkNamePadding)\n\t\t\tif n.CheckRun.Conclusion != nil {\n\t\t\t\tvar conclusionMarker string\n\t\t\t\tswitch *n.CheckRun.Conclusion {\n\t\t\t\tcase checkConclusionStateSuccess:\n\t\t\t\t\tconclusionMarker = \" ✅\"\n\t\t\t\tcase checkConclusionStateFailure, checkConclusionStateError:\n\t\t\t\t\tconclusionMarker = \" ❌\"\n\t\t\t\t}\n\t\t\t\tchecks = append(checks, fmt.Sprintf(\"- %s %s%s\",\n\t\t\t\t\tcheckName,\n\t\t\t\t\tRightPadTrim(fmt.Sprintf(\"`%s`\", *n.CheckRun.Conclusion), statusConclusionPadding),\n\t\t\t\t\tconclusionMarker,\n\t\t\t\t))\n\t\t\t} else {\n\t\t\t\tchecks = append(checks, fmt.Sprintf(\"- %s %s\", checkName, n.CheckRun.Status))\n\t\t\t}\n\t\tcase statusContextType:\n\t\t\tvar stateMarker string\n\t\t\tswitch n.StatusContext.State {\n\t\t\tcase statusStateSuccess:\n\t\t\t\tstateMarker = \" ✅\"\n\t\t\tcase statusStateFailure, statusStateError:\n\t\t\t\tstateMarker = \" ❌\"\n\t\t\t}\n\t\t\tchecks = append(checks, fmt.Sprintf(\"- %s %s%s\",\n\t\t\t\tRightPadTrim(n.StatusContext.Context, checkNamePadding),\n\t\t\t\tRightPadTrim(fmt.Sprintf(\"`%s`\", n.StatusContext.State), statusConclusionPadding),\n\t\t\t\tstateMarker,\n\t\t\t))\n\t\t}\n\t}\n\n\tif len(checks) == 0 {\n\t\treturn noChecksHeader\n\t}\n\n\treturn fmt.Sprintf(`\n## Checks\n\n%s **%s**\n\n%s`,\n\t\tRightPadTrim(\"> Status of latest commit\", checkNamePadding+2),\n\t\tpr.LastCommit.Nodes[0].Commit.StatusCheckRollup.State,\n\t\tstrings.Join(checks, \"\\n\"))\n}\n\nfunc (pr prDetails) References() string {\n\tissues := make([]string, len(pr.IssueReferences.Nodes))\n\tfor i, iss := range pr.IssueReferences.Nodes {\n\t\tissues[i] = fmt.Sprintf(\"- `#%d`: %s (%s)\", iss.Number, iss.Title, iss.URL)\n\t}\n\treturn fmt.Sprintf(`\n## Referenced by\n\n%s`, strings.Join(issues, \"\\n\"))\n}\n\nfunc (pr prDetails) FilesChanged() string {\n\tfc := make([]string, len(pr.Files.Nodes))\n\tfor i, f := range pr.Files.Nodes {\n\t\tvar additions string\n\t\tvar deletions string\n\n\t\tif f.Additions > 0 {\n\t\t\tadditions = fmt.Sprintf(\" `+%d`\", f.Additions)\n\t\t}\n\n\t\tif f.Deletions > 0 {\n\t\t\tdeletions = fmt.Sprintf(\" `-%d`\", f.Deletions)\n\t\t}\n\n\t\tfc[i] = fmt.Sprintf(\"- %s%s%s\", f.Path, additions, deletions)\n\t}\n\treturn fmt.Sprintf(`\n## Files changed\n\n%s`, strings.Join(fc, \"\\n\"))\n}\n\nfunc (pr prDetails) CommitsList() string {\n\tvar commitsStr string\n\n\tcommits := make([]string, len(pr.Commits.Nodes))\n\tfor i, c := range pr.Commits.Nodes {\n\t\thash := c.Commit.AbbreviatedOid\n\n\t\tcommits[i] = fmt.Sprintf(\"- `%s`: %s **(%s)** `<%s>`\",\n\t\t\thash,\n\t\t\tc.Commit.MessageHeadline,\n\t\t\thumanize.Time(c.Commit.AuthoredDate),\n\t\t\tc.Commit.Author.Name,\n\t\t)\n\t}\n\n\tvar commitsNumStr string\n\tif len(pr.Commits.Nodes) < pr.Commits.TotalCount {\n\t\tcommitsNumStr = fmt.Sprintf(\" (last %d out of %d)\", len(pr.Commits.Nodes), pr.Commits.TotalCount)\n\t}\n\n\tcommitsStr = fmt.Sprintf(`\n## Commits%s\n\n%s\n`, commitsNumStr, strings.Join(commits, \"\\n\"))\n\n\treturn commitsStr\n}\n\nfunc (pr prDetails) CommentsList() string {\n\tcomments := make([]string, len(pr.Comments.Nodes))\n\tfor i, c := range pr.Comments.Nodes {\n\t\tcomments[i] = fmt.Sprintf(\"`@%s` (%s):\\n\\n%s\", c.Author.Login, humanize.Time(c.UpdatedAt), c.Body)\n\t}\n\n\tvar commentsNumStr string\n\tif len(pr.Comments.Nodes) < pr.Comments.TotalCount {\n\t\tcommentsNumStr = fmt.Sprintf(\" (first %d out of %d)\", len(pr.Comments.Nodes), pr.Comments.TotalCount)\n\t}\n\n\treturn fmt.Sprintf(`\n## Comments%s\n\n%s\n`, commentsNumStr, strings.Join(comments, \"\\n\\n▬▬▬▬▬▬\\n\\n\"))\n}\n\nfunc (repo Repo) Title() string {\n\treturn repo.Name\n}\n\nfunc (repo Repo) Description() string {\n\treturn repo.Owner\n}\n\nfunc (repo Repo) FilterValue() string {\n\treturn fmt.Sprintf(\"%s:::%s\", repo.Owner, repo.Name)\n}\n\nfunc (prRes prResult) Title() string {\n\treturn prRes.title\n}\n\nfunc (prRes prResult) Description() string {\n\treturn prRes.description\n}\n\nfunc (prRes prResult) FilterValue() string {\n\treturn fmt.Sprintf(\"%d\", prRes.pr.Number)\n}\n\nfunc (ir prTLItemResult) Title() string {\n\treturn ir.title\n}\n\nfunc (ir prTLItemResult) Description() string {\n\treturn ir.description\n}\n\nfunc (ir prTLItemResult) FilterValue() string {\n\treturn ir.title\n}\n"
  },
  {
    "path": "ui/ui.go",
    "content": "package ui\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\tghapi \"github.com/cli/go-gh/v2/pkg/api\"\n)\n\nfunc RenderUI(ghClient *ghapi.GraphQLClient, config Config, mode Mode) error {\n\tif len(os.Getenv(\"DEBUG\")) > 0 {\n\t\tf, err := tea.LogToFile(\"debug.log\", \"debug\")\n\t\tif err != nil {\n\t\t\tfmt.Println(\"fatal:\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t\tdefer f.Close()\n\t}\n\tp := tea.NewProgram(InitialModel(ghClient, config, mode))\n\t_, err := p.Run()\n\treturn err\n}\n"
  },
  {
    "path": "ui/update.go",
    "content": "package ui\n\nimport (\n\t_ \"embed\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"charm.land/bubbles/v2/list\"\n\t\"charm.land/bubbles/v2/viewport\"\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n\t\"github.com/dhth/prs/internal/utils\"\n)\n\nconst (\n\tviewPortMoveLineCount  = 5\n\tcouldntGetPRDetailsMsg = \"Couldn't get repo/pr details. Inform @dhth on Github.\"\n)\n\nvar (\n\t//go:embed assets/help.md\n\thelpStr string\n\n\tErrPRDetailsNotCached = errors.New(\"PR details were not saved\")\n)\n\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar cmd tea.Cmd\n\tvar cmds []tea.Cmd\n\tm.message = \"\"\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyPressMsg:\n\t\tswitch msg.String() {\n\t\tcase \"Q\":\n\t\t\treturn m, tea.Quit\n\t\tcase \"ctrl+c\", \"q\", \"esc\":\n\t\t\tswitch m.activePane {\n\t\t\tcase repoListView:\n\t\t\t\tif !m.repoChosen {\n\t\t\t\t\treturn m, tea.Quit\n\t\t\t\t}\n\t\t\t\tm.activePane = m.lastPane\n\t\t\tcase helpView:\n\t\t\t\tm.activePane = m.lastPane\n\t\t\tcase prTLItemDetailView:\n\t\t\t\tm.prTLItemDetailVP.GotoTop()\n\t\t\t\tm.activePane = prTLListView\n\t\t\tcase prTLListView:\n\t\t\t\tm.prTLList.ResetSelected()\n\t\t\t\tm.activePane = prListView\n\t\t\tcase prDetailsView:\n\t\t\t\tif m.lastPane == m.activePane {\n\t\t\t\t\tm.activePane = m.secondLastActivePane\n\t\t\t\t\tm.lastPane = prDetailsView\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tm.activePane = m.lastPane\n\t\t\t\tm.lastPane = prDetailsView\n\t\t\tcase prListView:\n\t\t\t\tif m.mode == RepoMode {\n\t\t\t\t\tm.activePane = repoListView\n\t\t\t\t\tm.repoChosen = false\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\treturn m, tea.Quit\n\t\t\tdefault:\n\t\t\t\treturn m, tea.Quit\n\t\t\t}\n\n\t\tcase \"ctrl+r\":\n\t\t\tswitch m.activePane {\n\t\t\tcase prListView:\n\n\t\t\t\tswitch m.mode {\n\t\t\t\tcase RepoMode:\n\t\t\t\t\tcmds = append(cmds, fetchPRSForRepo(m.ghClient, m.repoOwner, m.repoName, m.config.PRCount))\n\t\t\t\tcase QueryMode:\n\t\t\t\t\tcmds = append(cmds, fetchPRSFromQuery(m.ghClient, *m.config.Query, m.config.PRCount))\n\t\t\t\t}\n\t\t\t\tm.prsList.Title = fetchingPRsTitle\n\t\t\t\tm.prsList.Styles.Title = m.prsList.Styles.Title.Background(lipgloss.Color(fetchingColor))\n\n\t\t\tcase prTLListView:\n\t\t\t\tpr, ok := m.prsList.SelectedItem().(*prResult)\n\t\t\t\tif !ok {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\trepoOwner := pr.pr.Repository.Owner.Login\n\t\t\t\trepoName := pr.pr.Repository.Name\n\t\t\t\tprNumber := pr.pr.Number\n\t\t\t\tcmds = append(cmds, fetchPRTLItems(m.ghClient, repoOwner, repoName, prNumber, 100, true))\n\t\t\t\tm.prTLList.Title = \"fetching timeline...\"\n\t\t\t\tm.prTLList.Styles.Title = m.prTLList.Styles.Title.Background(lipgloss.Color(fetchingColor))\n\t\t\t}\n\t\tcase \"1\":\n\t\t\tif m.activePane != prTLListView && m.activePane != prTLItemDetailView && m.activePane != prDetailsView {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tswitch m.activePane {\n\t\t\tcase prDetailsView:\n\t\t\t\tm.GoToPRDetailSection(0)\n\t\t\tdefault:\n\t\t\t\tm.activePane = prListView\n\t\t\t}\n\n\t\tcase \"enter\":\n\t\t\tswitch m.activePane {\n\t\t\tcase prListView:\n\t\t\t\tsetTlCmd, ok := m.setTL()\n\t\t\t\tif !ok {\n\t\t\t\t\tm.message = couldntGetPRDetailsMsg\n\t\t\t\t} else {\n\t\t\t\t\tif setTlCmd != nil {\n\t\t\t\t\t\tcmds = append(cmds, setTlCmd)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tcase prTLListView:\n\t\t\t\titem, ok := m.prTLList.SelectedItem().(*prTLItemResult)\n\t\t\t\tif !ok {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tif item.item.Type != tlItemPRReview {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tif len(item.item.PullRequestReview.Comments.Nodes) == 0 {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tm.setPRReviewCmt(item.item, 0)\n\t\t\t\tm.prRevCurCmtNum = 0\n\t\t\t\tm.activePane = prTLItemDetailView\n\n\t\t\tcase repoListView:\n\t\t\t\tselected := m.repoList.SelectedItem()\n\t\t\t\tif selected != nil {\n\t\t\t\t\tcmds = append(cmds, chooseRepo(selected.FilterValue()))\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"2\":\n\t\t\tif m.activePane != prListView && m.activePane != prTLItemDetailView && m.activePane != prDetailsView {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tswitch m.activePane {\n\t\t\tcase prDetailsView:\n\t\t\t\tm.GoToPRDetailSection(1)\n\t\t\tdefault:\n\t\t\t\tsetTlCmd, ok := m.setTL()\n\t\t\t\tif !ok {\n\t\t\t\t\tm.message = \"Could't get repo/pr details. Inform @dhth on github.\"\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tif setTlCmd != nil {\n\t\t\t\t\tcmds = append(cmds, setTlCmd)\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase \"3\":\n\t\t\tif m.activePane != prTLListView && m.activePane != prDetailsView {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tswitch m.activePane {\n\t\t\tcase prDetailsView:\n\t\t\t\tm.GoToPRDetailSection(2)\n\t\t\tdefault:\n\t\t\t\ttlItem, ok := m.prTLList.SelectedItem().(*prTLItemResult)\n\t\t\t\tif !ok {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tif tlItem.item.Type != tlItemPRReview {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tif len(tlItem.item.PullRequestReview.Comments.Nodes) == 0 {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tm.setPRReviewCmt(tlItem.item, 0)\n\t\t\t\tm.activePane = prTLItemDetailView\n\t\t\t}\n\n\t\tcase \"4\":\n\t\t\tif m.activePane != prDetailsView {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tm.GoToPRDetailSection(3)\n\n\t\tcase \"5\":\n\t\t\tif m.activePane != prDetailsView {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tm.GoToPRDetailSection(4)\n\n\t\tcase \"6\":\n\t\t\tif m.activePane != prDetailsView {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tm.GoToPRDetailSection(5)\n\n\t\tcase \"j\", \"down\":\n\t\t\tif m.activePane != prTLItemDetailView && m.activePane != helpView && m.activePane != prDetailsView {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tswitch m.activePane {\n\t\t\tcase prTLItemDetailView:\n\t\t\t\tif m.prTLItemDetailVP.AtBottom() {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tm.prTLItemDetailVP.ScrollDown(viewPortMoveLineCount)\n\t\t\tcase prDetailsView:\n\t\t\t\tif m.prDetailsVP.AtBottom() {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tm.prDetailsVP.ScrollUp(viewPortMoveLineCount)\n\t\t\tcase helpView:\n\t\t\t\tif m.helpVP.AtBottom() {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tm.helpVP.ScrollDown(viewPortMoveLineCount)\n\t\t\t}\n\n\t\tcase \"k\", \"up\":\n\t\t\tif m.activePane != prTLItemDetailView && m.activePane != helpView && m.activePane != prDetailsView {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tswitch m.activePane {\n\t\t\tcase prTLItemDetailView:\n\t\t\t\tif m.prTLItemDetailVP.AtTop() {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tm.prTLItemDetailVP.ScrollUp(viewPortMoveLineCount)\n\t\t\tcase prDetailsView:\n\t\t\t\tif m.prDetailsVP.AtTop() {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tm.prDetailsVP.ScrollUp(viewPortMoveLineCount)\n\t\t\tcase helpView:\n\t\t\t\tif m.helpVP.AtTop() {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tm.helpVP.ScrollUp(viewPortMoveLineCount)\n\t\t\t}\n\n\t\tcase \"tab\", \"shift+tab\":\n\t\t\tif m.activePane == helpView || m.activePane == prDetailsView {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif m.activePane == prListView {\n\t\t\t\tsetTlCmd, ok := m.setTL()\n\t\t\t\tif !ok {\n\t\t\t\t\tm.message = \"Could't get repo/pr details. Inform @dhth on github.\"\n\t\t\t\t} else {\n\t\t\t\t\tif setTlCmd != nil {\n\t\t\t\t\t\tcmds = append(cmds, setTlCmd)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tm.activePane = prListView\n\t\t\t}\n\t\tcase \"ctrl+s\":\n\t\t\tif m.mode == RepoMode {\n\t\t\t\tif m.activePane != repoListView {\n\t\t\t\t\tm.lastPane = m.activePane\n\t\t\t\t\tm.activePane = repoListView\n\t\t\t\t} else {\n\t\t\t\t\tm.activePane = m.lastPane\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase \"ctrl+b\":\n\t\t\tswitch m.activePane {\n\t\t\tcase prListView, prDetailsView:\n\t\t\t\tpr, ok := m.prsList.SelectedItem().(*prResult)\n\t\t\t\tif !ok {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tcmds = append(cmds, openURLInBrowser(pr.pr.URL))\n\t\t\tcase prTLListView, prTLItemDetailView:\n\t\t\t\titem, ok := m.prTLList.SelectedItem().(*prTLItemResult)\n\t\t\t\tif !ok {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tswitch item.item.Type {\n\t\t\t\tcase tlItemPRCommit:\n\t\t\t\t\tcmds = append(cmds, openURLInBrowser(item.item.PullRequestCommit.URL))\n\t\t\t\tcase tlItemHeadRefForcePushed:\n\t\t\t\t\tcmds = append(cmds, openURLInBrowser(item.item.HeadRefForcePushed.AfterCommit.URL))\n\t\t\t\tcase tlItemPRReview:\n\t\t\t\t\tcmds = append(cmds, openURLInBrowser(item.item.PullRequestReview.URL))\n\t\t\t\tcase tlItemMergedEvent:\n\t\t\t\t\tcmds = append(cmds, openURLInBrowser(item.item.MergedEvent.URL))\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase \"ctrl+d\":\n\t\t\tif m.activePane != prListView && m.activePane != prTLListView {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tpr, ok := m.prsList.SelectedItem().(*prResult)\n\t\t\tif !ok {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tcmds = append(cmds, showDiff(pr.pr.Repository.Owner.Login,\n\t\t\t\tpr.pr.Repository.Name,\n\t\t\t\tpr.pr.Number))\n\n\t\tcase \"ctrl+v\":\n\t\t\tif m.activePane == helpView {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tpr, ok := m.prsList.SelectedItem().(*prResult)\n\t\t\tif !ok {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tcmds = append(cmds, showPR(pr.pr.Repository.Owner.Login,\n\t\t\t\tpr.pr.Repository.Name,\n\t\t\t\tpr.pr.Number))\n\n\t\tcase \"g\":\n\t\t\tswitch m.activePane {\n\t\t\tcase prTLItemDetailView:\n\t\t\t\tm.prTLItemDetailVP.GotoTop()\n\t\t\tcase prDetailsView:\n\t\t\t\tm.prDetailsVP.GotoTop()\n\t\t\tcase helpView:\n\t\t\t\tm.helpVP.GotoTop()\n\t\t\t}\n\t\tcase \"G\":\n\t\t\tswitch m.activePane {\n\t\t\tcase prTLItemDetailView:\n\t\t\t\tm.prTLItemDetailVP.GotoBottom()\n\t\t\tcase prDetailsView:\n\t\t\t\tm.prDetailsVP.GotoBottom()\n\t\t\tcase helpView:\n\t\t\t\tm.helpVP.GotoBottom()\n\t\t\t}\n\n\t\tcase \"K\", \"[\":\n\t\t\tif m.activePane != prDetailsView {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tm.prsList.CursorUp()\n\t\t\tprRes, ok := m.prsList.SelectedItem().(*prResult)\n\t\t\tif !ok {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tprDetails, ok := m.prDetailsCache[prRes.identifier]\n\t\t\tif !ok {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tvar section uint\n\t\t\tlastSection, ok := m.prDetailsCurSectionCache[prRes.identifier]\n\t\t\tif ok {\n\t\t\t\tsection = lastSection\n\t\t\t} else {\n\t\t\t\tsection = 0\n\t\t\t}\n\n\t\t\tm.setPRDetailsContent(prDetails, PRDetailsSectionList[section])\n\t\t\tm.prDetailsCurrentSection = section\n\n\t\tcase \"J\", \"]\":\n\t\t\tif m.activePane != prDetailsView {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tm.prsList.CursorDown()\n\t\t\tprRes, ok := m.prsList.SelectedItem().(*prResult)\n\t\t\tif !ok {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tprDetails, ok := m.prDetailsCache[prRes.identifier]\n\t\t\tif !ok {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tvar section uint\n\t\t\tlastSection, ok := m.prDetailsCurSectionCache[prRes.identifier]\n\t\t\tif ok {\n\t\t\t\tsection = lastSection\n\t\t\t} else {\n\t\t\t\tsection = 0\n\t\t\t}\n\n\t\t\tm.setPRDetailsContent(prDetails, PRDetailsSectionList[section])\n\t\t\tm.prDetailsCurrentSection = section\n\n\t\tcase \"d\":\n\t\t\tif m.activePane != prListView && m.activePane != prDetailsView && m.activePane != prTLListView && m.activePane != prTLItemDetailView {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif m.activePane == prDetailsView {\n\t\t\t\tm.activePane = m.lastPane\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tprRes, ok := m.prsList.SelectedItem().(*prResult)\n\t\t\tif !ok {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tprDetails, ok := m.prDetailsCache[prRes.identifier]\n\t\t\tif !ok {\n\t\t\t\tm.message = \"PR details were not retrieved\"\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tvar section uint\n\t\t\tlastSection, ok := m.prDetailsCurSectionCache[prRes.identifier]\n\t\t\tif ok {\n\t\t\t\tsection = lastSection\n\t\t\t} else {\n\t\t\t\tsection = 0\n\t\t\t}\n\n\t\t\tm.setPRDetailsContent(prDetails, PRDetailsSectionList[section])\n\t\t\tm.prDetailsCurrentSection = section\n\n\t\t\tm.prDetailsVP.GotoTop()\n\t\t\tm.lastPane = m.activePane\n\t\t\tm.activePane = prDetailsView\n\n\t\tcase \"l\", \"n\", \"right\":\n\t\t\tif m.activePane != prDetailsView && m.activePane != prTLItemDetailView {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tswitch m.activePane {\n\t\t\tcase prDetailsView:\n\t\t\t\tprRes, ok := m.prsList.SelectedItem().(*prResult)\n\t\t\t\tif !ok {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tprDetails, ok := m.prDetailsCache[prRes.identifier]\n\t\t\t\tif !ok {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tnextSectionFound := false\n\t\t\t\tvar nextSection uint\n\t\t\t\tif m.prDetailsCurrentSection == uint(len(PRDetailsSectionList)-1) {\n\t\t\t\t\tnextSection = 0\n\t\t\t\t} else {\n\t\t\t\t\tnextSection = m.prDetailsCurrentSection + 1\n\t\t\t\t}\n\n\t\t\t\tfor {\n\t\t\t\t\tswitch nextSection {\n\t\t\t\t\tcase 0:\n\t\t\t\t\t\tnextSectionFound = true\n\t\t\t\t\tcase 1:\n\t\t\t\t\t\tif prDetails.Body != \"\" {\n\t\t\t\t\t\t\tnextSectionFound = true\n\t\t\t\t\t\t}\n\t\t\t\t\tcase 2:\n\t\t\t\t\t\t// this may still lead to no status checks being shown\n\t\t\t\t\t\t// but the probability of that happening is pretty low\n\t\t\t\t\t\tif len(prDetails.LastCommit.Nodes) > 0 && prDetails.LastCommit.Nodes[0].Commit.StatusCheckRollup != nil {\n\t\t\t\t\t\t\tnextSectionFound = true\n\t\t\t\t\t\t}\n\t\t\t\t\tcase 3:\n\t\t\t\t\t\tif len(prDetails.IssueReferences.Nodes) > 0 {\n\t\t\t\t\t\t\tnextSectionFound = true\n\t\t\t\t\t\t}\n\t\t\t\t\tcase 4:\n\t\t\t\t\t\tif len(prDetails.Files.Nodes) > 0 {\n\t\t\t\t\t\t\tnextSectionFound = true\n\t\t\t\t\t\t}\n\t\t\t\t\tcase 5:\n\t\t\t\t\t\tif len(prDetails.Commits.Nodes) > 0 {\n\t\t\t\t\t\t\tnextSectionFound = true\n\t\t\t\t\t\t}\n\t\t\t\t\tcase 6:\n\t\t\t\t\t\tif len(prDetails.Comments.Nodes) > 0 {\n\t\t\t\t\t\t\tnextSectionFound = true\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tnextSection = 0\n\t\t\t\t\t\t\tnextSectionFound = true\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif nextSectionFound {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\n\t\t\t\t\tnextSection++\n\t\t\t\t}\n\n\t\t\t\tif !nextSectionFound {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tif nextSection > uint(len(PRDetailsSectionList)-1) {\n\t\t\t\t\tm.message = \"Something went wrong\"\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tm.setPRDetailsContent(prDetails, PRDetailsSectionList[nextSection])\n\t\t\t\tm.prDetailsCurSectionCache[prRes.identifier] = nextSection\n\t\t\t\tm.prDetailsCurrentSection = nextSection\n\n\t\t\tcase prTLItemDetailView:\n\t\t\t\ttlItem, ok := m.prTLList.SelectedItem().(*prTLItemResult)\n\t\t\t\tif !ok {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tif tlItem.item.Type != tlItemPRReview {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tif len(tlItem.item.PullRequestReview.Comments.Nodes) <= 1 {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tnextCommentIndex := m.prRevCurCmtNum + 1\n\t\t\t\tif nextCommentIndex > uint(len(tlItem.item.PullRequestReview.Comments.Nodes))-1 {\n\t\t\t\t\tnextCommentIndex = 0\n\t\t\t\t}\n\n\t\t\t\tm.setPRReviewCmt(tlItem.item, nextCommentIndex)\n\t\t\t\tm.prRevCurCmtNum = nextCommentIndex\n\t\t\t}\n\n\t\tcase \"h\", \"N\", \"left\":\n\t\t\tif m.activePane != prDetailsView && m.activePane != prTLItemDetailView {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tswitch m.activePane {\n\t\t\tcase prDetailsView:\n\t\t\t\tprRes, ok := m.prsList.SelectedItem().(*prResult)\n\t\t\t\tif !ok {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tprDetails, ok := m.prDetailsCache[prRes.identifier]\n\t\t\t\tif !ok {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tprevSectionFound := false\n\t\t\t\tvar prevSection uint\n\t\t\t\tif m.prDetailsCurrentSection == 0 {\n\t\t\t\t\tprevSection = uint(len(PRDetailsSectionList) - 1)\n\t\t\t\t} else {\n\t\t\t\t\tprevSection = m.prDetailsCurrentSection - 1\n\t\t\t\t}\n\n\t\t\t\tfor {\n\t\t\t\t\tswitch prevSection {\n\t\t\t\t\tcase 0:\n\t\t\t\t\t\tprevSectionFound = true\n\t\t\t\t\tcase 1:\n\t\t\t\t\t\tif prDetails.Body != \"\" {\n\t\t\t\t\t\t\tprevSectionFound = true\n\t\t\t\t\t\t}\n\t\t\t\t\tcase 2:\n\t\t\t\t\t\tif len(prDetails.LastCommit.Nodes) > 0 && prDetails.LastCommit.Nodes[0].Commit.StatusCheckRollup != nil {\n\t\t\t\t\t\t\tprevSectionFound = true\n\t\t\t\t\t\t}\n\t\t\t\t\tcase 3:\n\t\t\t\t\t\tif len(prDetails.IssueReferences.Nodes) > 0 {\n\t\t\t\t\t\t\tprevSectionFound = true\n\t\t\t\t\t\t}\n\t\t\t\t\tcase 4:\n\t\t\t\t\t\tif len(prDetails.Files.Nodes) > 0 {\n\t\t\t\t\t\t\tprevSectionFound = true\n\t\t\t\t\t\t}\n\t\t\t\t\tcase 5:\n\t\t\t\t\t\tif len(prDetails.Commits.Nodes) > 0 {\n\t\t\t\t\t\t\tprevSectionFound = true\n\t\t\t\t\t\t}\n\t\t\t\t\tcase 6:\n\t\t\t\t\t\tif len(prDetails.Comments.Nodes) > 0 {\n\t\t\t\t\t\t\tprevSectionFound = true\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif prevSectionFound {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\n\t\t\t\t\tprevSection--\n\t\t\t\t}\n\n\t\t\t\tm.setPRDetailsContent(prDetails, PRDetailsSectionList[prevSection])\n\t\t\t\tm.prDetailsCurSectionCache[prRes.identifier] = prevSection\n\t\t\t\tm.prDetailsCurrentSection = prevSection\n\n\t\t\tcase prTLItemDetailView:\n\t\t\t\ttlItem, ok := m.prTLList.SelectedItem().(*prTLItemResult)\n\t\t\t\tif !ok {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tif tlItem.item.Type != tlItemPRReview {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tif len(tlItem.item.PullRequestReview.Comments.Nodes) <= 1 {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tvar prevCommentIndex uint\n\t\t\t\tif m.prRevCurCmtNum == 0 {\n\t\t\t\t\tprevCommentIndex = uint(len(tlItem.item.PullRequestReview.Comments.Nodes) - 1)\n\t\t\t\t} else {\n\t\t\t\t\tprevCommentIndex = m.prRevCurCmtNum - 1\n\t\t\t\t}\n\n\t\t\t\tm.setPRReviewCmt(tlItem.item, prevCommentIndex)\n\t\t\t\tm.prRevCurCmtNum = prevCommentIndex\n\t\t\t}\n\n\t\tcase \"?\":\n\t\t\tif m.activePane == helpView {\n\t\t\t\tm.activePane = m.lastPane\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif m.activePane == prDetailsView {\n\t\t\t\tm.secondLastActivePane = m.lastPane\n\t\t\t}\n\n\t\t\tm.lastPane = m.activePane\n\t\t\tm.activePane = helpView\n\t\t}\n\tcase hideHelpMsg:\n\t\tm.showHelp = false\n\tcase tea.WindowSizeMsg:\n\t\tw, h := listStyle.GetFrameSize()\n\t\tm.terminalDetails.width = msg.Width\n\n\t\tif m.mode == RepoMode {\n\t\t\tm.repoList.SetHeight(msg.Height - h - 2)\n\t\t\tm.repoList.SetWidth(msg.Width - w)\n\t\t}\n\n\t\tm.prsList.SetHeight(msg.Height - h - 2)\n\t\tm.prsList.SetWidth(msg.Width - w)\n\n\t\tm.prTLList.SetHeight(msg.Height - h - 2)\n\t\tm.prTLList.SetWidth(msg.Width - w)\n\n\t\tif !m.prTLItemDetailVPReady {\n\t\t\tm.prTLItemDetailVP = viewport.New(\n\t\t\t\tviewport.WithWidth(msg.Width-2),\n\t\t\t\tviewport.WithHeight(msg.Height-7),\n\t\t\t)\n\t\t\tm.prTLItemDetailVPReady = true\n\t\t\tm.prTLItemDetailVP.KeyMap.HalfPageDown.SetKeys(\"ctrl+d\")\n\t\t} else {\n\t\t\tm.prTLItemDetailVP.SetWidth(msg.Width - 2)\n\t\t\tm.prTLItemDetailVP.SetHeight(msg.Height - 7)\n\t\t}\n\n\t\tif !m.prDetailsVPReady {\n\t\t\tm.prDetailsVP = viewport.New(\n\t\t\t\tviewport.WithWidth(msg.Width-2),\n\t\t\t\tviewport.WithHeight(msg.Height-7),\n\t\t\t)\n\t\t\tm.prDetailsVPReady = true\n\t\t\tm.prDetailsVP.KeyMap.HalfPageDown.SetKeys(\"ctrl+d\")\n\t\t} else {\n\t\t\tm.prDetailsVP.SetWidth(msg.Width - 2)\n\t\t\tm.prDetailsVP.SetHeight(msg.Height - 7)\n\t\t}\n\n\t\tvpWrap := min((msg.Width - 4), viewPortWrapUpperLimit)\n\n\t\tm.mdRenderer, _ = utils.GetMarkDownRenderer(vpWrap)\n\n\t\thelpToRender := helpStr\n\t\tif m.mdRenderer != nil {\n\t\t\thelpStrGl, err := m.mdRenderer.Render(helpStr)\n\t\t\tif err != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\thelpToRender = helpStrGl\n\t\t}\n\n\t\tif !m.helpVPReady {\n\t\t\tm.helpVP = viewport.New(\n\t\t\t\tviewport.WithWidth(msg.Width),\n\t\t\t\tviewport.WithHeight(msg.Height-7),\n\t\t\t)\n\t\t\tm.helpVP.SetContent(helpToRender)\n\t\t\tm.helpVPReady = true\n\t\t} else {\n\t\t\tm.helpVP.SetWidth(msg.Width)\n\t\t\tm.helpVP.SetHeight(msg.Height - 7)\n\t\t}\n\n\t\tprs := make([]list.Item, len(m.prCache))\n\t\tfor i := range m.prCache {\n\t\t\tm.prCache[i].title = getPRTitle(m.prCache[i].pr)\n\t\t\tm.prCache[i].description = getPRDesc(m.prCache[i].pr, m.mode, m.terminalDetails)\n\t\t\tprs[i] = m.prCache[i]\n\t\t}\n\t\tm.prsList.SetItems(prs)\n\n\t\tif m.activePane == prTLListView {\n\t\t\tm.setTL()\n\t\t}\n\n\tcase repoChosenMsg:\n\t\trepoDetails := strings.Split(msg.repo, \":::\")\n\t\tif len(repoDetails) != 2 {\n\t\t\tm.message = \"Something went horribly wrong. Let @dhth know about this failure.\"\n\t\t} else {\n\t\t\tm.repoChosen = true\n\t\t\tm.prsList.Title = fetchingPRsTitle\n\t\t\tm.prsList.Styles.Title = m.prsList.Styles.Title.Background(lipgloss.Color(fetchingColor))\n\t\t\tm.repoOwner = repoDetails[0]\n\t\t\tm.repoName = repoDetails[1]\n\t\t\tm.activePane = prListView\n\t\t\tm.prsList.ResetSelected()\n\t\t\tm.prTLList.ResetSelected()\n\t\t\tcmds = append(cmds, fetchPRSForRepo(m.ghClient, m.repoOwner, m.repoName, m.config.PRCount))\n\t\t}\n\tcase prsFetchedMsg:\n\t\tif msg.err != nil {\n\t\t\tm.message = msg.err.Error()\n\t\t\tm.prsList.Title = \"error\"\n\t\t\tbreak\n\t\t}\n\n\t\tprs := make([]list.Item, len(msg.prs))\n\t\tprResults := make([]*prResult, len(msg.prs))\n\t\tm.prDetailsCurSectionCache = make(map[string]uint)\n\n\t\tfor i, pr := range msg.prs {\n\t\t\tprResults[i] = &prResult{\n\t\t\t\tpr:          &pr,\n\t\t\t\ttitle:       getPRTitle(&pr),\n\t\t\t\tdescription: getPRDesc(&pr, m.mode, m.terminalDetails),\n\t\t\t\tidentifier:  fmt.Sprintf(\"%s/%s:%d\", pr.Repository.Owner.Login, pr.Repository.Name, pr.Number),\n\t\t\t}\n\t\t\tprs[i] = prResults[i]\n\t\t}\n\n\t\tm.prCache = prResults\n\t\tm.prsList.SetItems(prs)\n\n\t\tswitch m.mode {\n\t\tcase RepoMode:\n\t\t\tm.prsList.Title = fmt.Sprintf(\"PRs (%s)\", m.repoName)\n\t\tcase QueryMode:\n\t\t\tm.prsList.Title = \"Results\"\n\t\t}\n\n\t\tm.prsList.ResetSelected()\n\t\tm.prsList.Styles.Title = m.prsList.Styles.Title.Background(lipgloss.Color(prListColor))\n\n\t\tfor _, pr := range msg.prs {\n\t\t\tcmds = append(cmds, fetchPRTLItems(m.ghClient,\n\t\t\t\tpr.Repository.Owner.Login,\n\t\t\t\tpr.Repository.Name,\n\t\t\t\tpr.Number,\n\t\t\t\t100,\n\t\t\t\tfalse,\n\t\t\t))\n\t\t\tcmds = append(cmds, fetchPRMetadata(m.ghClient,\n\t\t\t\tpr.Repository.Owner.Login,\n\t\t\t\tpr.Repository.Name,\n\t\t\t\tpr.Number,\n\t\t\t))\n\t\t}\n\n\tcase reviewPRsFetchedMsg:\n\t\tif msg.err != nil {\n\t\t\tm.message = msg.err.Error()\n\t\t\tbreak\n\t\t}\n\n\t\tprs := make([]list.Item, len(msg.prs))\n\t\tprResults := make([]*prResult, len(msg.prs))\n\t\tm.prDetailsCurSectionCache = make(map[string]uint)\n\n\t\tfor i, pr := range msg.prs {\n\t\t\tprResults[i] = &prResult{\n\t\t\t\tpr:          &pr,\n\t\t\t\ttitle:       getPRTitle(&pr),\n\t\t\t\tdescription: getPRDesc(&pr, m.mode, m.terminalDetails),\n\t\t\t\tidentifier:  fmt.Sprintf(\"%s/%s:%d\", pr.Repository.Owner.Login, pr.Repository.Name, pr.Number),\n\t\t\t}\n\t\t\tprs[i] = prResults[i]\n\t\t}\n\n\t\tm.prCache = prResults\n\t\tm.prsList.SetItems(prs)\n\t\tm.prsList.ResetSelected()\n\t\tm.prsList.Title = \"Open PRs requesting your review\"\n\t\tm.prsList.Styles.Title = m.prsList.Styles.Title.Background(lipgloss.Color(prListColor))\n\n\t\tif len(msg.prs) > 0 {\n\t\t\tfor _, pr := range msg.prs {\n\t\t\t\tcmds = append(cmds, fetchPRTLItems(m.ghClient, pr.Repository.Owner.Login, pr.Repository.Name, pr.Number, 100, false))\n\t\t\t\tcmds = append(cmds, fetchPRMetadata(m.ghClient,\n\t\t\t\t\tpr.Repository.Owner.Login,\n\t\t\t\t\tpr.Repository.Name,\n\t\t\t\t\tpr.Number,\n\t\t\t\t))\n\t\t\t}\n\t\t}\n\tcase authoredPRsFetchedMsg:\n\t\tif msg.err != nil {\n\t\t\tm.message = msg.err.Error()\n\t\t\tbreak\n\t\t}\n\n\t\tprs := make([]list.Item, len(msg.prs))\n\t\tprResults := make([]*prResult, len(msg.prs))\n\t\tm.prDetailsCurSectionCache = make(map[string]uint)\n\n\t\tfor i, pr := range msg.prs {\n\t\t\tprResults[i] = &prResult{\n\t\t\t\tpr:          &pr,\n\t\t\t\ttitle:       getPRTitle(&pr),\n\t\t\t\tdescription: getPRDesc(&pr, m.mode, m.terminalDetails),\n\t\t\t\tidentifier:  fmt.Sprintf(\"%s/%s:%d\", pr.Repository.Owner.Login, pr.Repository.Name, pr.Number),\n\t\t\t}\n\t\t\tprs[i] = prResults[i]\n\t\t}\n\n\t\tm.prCache = prResults\n\t\tm.prsList.SetItems(prs)\n\t\tm.prsList.Title = \"Open PRs authored by you\"\n\t\tm.prsList.ResetSelected()\n\t\tm.prsList.Styles.Title = m.prsList.Styles.Title.Background(lipgloss.Color(prListColor))\n\n\t\tif len(msg.prs) > 0 {\n\t\t\tfor _, pr := range msg.prs {\n\t\t\t\tcmds = append(cmds, fetchPRTLItems(m.ghClient, pr.Repository.Owner.Login, pr.Repository.Name, pr.Number, 100, false))\n\t\t\t\tcmds = append(cmds, fetchPRMetadata(m.ghClient,\n\t\t\t\t\tpr.Repository.Owner.Login,\n\t\t\t\t\tpr.Repository.Name,\n\t\t\t\t\tpr.Number,\n\t\t\t\t))\n\t\t\t}\n\t\t}\n\n\tcase prMetadataFetchedMsg:\n\t\tif msg.err != nil {\n\t\t\tm.message = msg.err.Error()\n\t\t\tbreak\n\t\t}\n\n\t\tm.prDetailsCache[fmt.Sprintf(\"%s/%s:%d\", msg.repoOwner, msg.repoName, msg.prNumber)] = msg.metadata\n\n\tcase prTLFetchedMsg:\n\t\tif msg.err != nil {\n\t\t\tm.message = msg.err.Error()\n\t\t\tbreak\n\t\t}\n\n\t\ttlItemsResult := make([]*prTLItemResult, len(msg.prTLItems))\n\n\t\tfor i, item := range msg.prTLItems {\n\t\t\ttlItemsResult[i] = &prTLItemResult{\n\t\t\t\titem:        &item,\n\t\t\t\ttitle:       getPRTLItemTitle(&item),\n\t\t\t\tdescription: getPRTLItemDesc(&item),\n\t\t\t}\n\t\t}\n\t\tm.prTLCache[fmt.Sprintf(\"%s/%s:%d\", msg.repoOwner, msg.repoName, msg.prNumber)] = tlItemsResult\n\n\t\tif msg.setItems {\n\t\t\tprTLItems := make([]list.Item, len(msg.prTLItems))\n\t\t\tfor i, result := range tlItemsResult {\n\t\t\t\tprTLItems[i] = result\n\t\t\t}\n\t\t\tm.prTLList.SetItems(prTLItems)\n\t\t\tm.prTLList.Title = fmt.Sprintf(\"PR #%d Timeline\", msg.prNumber)\n\t\t\tm.prTLList.Styles.Title = m.prTLList.Styles.Title.Background(lipgloss.Color(prTLListColor))\n\t\t\tm.activePane = prTLListView\n\t\t}\n\n\t\tm.prTLList.ResetSelected()\n\n\tcase urlOpenedinBrowserMsg:\n\t\tif msg.err != nil {\n\t\t\tm.message = fmt.Sprintf(\"Error opening url: %s\", msg.err.Error())\n\t\t}\n\tcase prDiffDoneMsg:\n\t\tif msg.err != nil {\n\t\t\tm.message = fmt.Sprintf(\"Error opening diff (is gh installed?): %s\", msg.err.Error())\n\t\t}\n\tcase prViewDoneMsg:\n\t\tif msg.err != nil {\n\t\t\tm.message = fmt.Sprintf(\"Error showing PR details (is gh installed?): %s\", msg.err.Error())\n\t\t}\n\t}\n\n\tswitch m.activePane {\n\tcase prListView:\n\t\tm.prsList, cmd = m.prsList.Update(msg)\n\t\tcmds = append(cmds, cmd)\n\tcase prTLListView:\n\t\tm.prTLList, cmd = m.prTLList.Update(msg)\n\t\tcmds = append(cmds, cmd)\n\tcase prDetailsView:\n\t\tm.prDetailsVP, cmd = m.prDetailsVP.Update(msg)\n\t\tcmds = append(cmds, cmd)\n\tcase prTLItemDetailView:\n\t\tm.prTLItemDetailVP, cmd = m.prTLItemDetailVP.Update(msg)\n\t\tcmds = append(cmds, cmd)\n\tcase repoListView:\n\t\tm.repoList, cmd = m.repoList.Update(msg)\n\t\tcmds = append(cmds, cmd)\n\tcase helpView:\n\t\tm.helpVP, cmd = m.helpVP.Update(msg)\n\t\tcmds = append(cmds, cmd)\n\t}\n\n\treturn m, tea.Batch(cmds...)\n}\n\nfunc (m *Model) setTL() (tea.Cmd, bool) {\n\tvar cmd tea.Cmd\n\tvar repoOwner, repoName string\n\tvar prNumber int\n\n\tprRes, prOk := m.prsList.SelectedItem().(*prResult)\n\tif !prOk {\n\t\treturn nil, false\n\t}\n\n\trepoOwner = prRes.pr.Repository.Owner.Login\n\trepoName = prRes.pr.Repository.Name\n\tprNumber = prRes.pr.Number\n\n\ttlFromCache, ok := m.prTLCache[prRes.identifier]\n\tif !ok {\n\t\tcmd = fetchPRTLItems(m.ghClient, repoOwner, repoName, prNumber, 100, true)\n\t\treturn cmd, true\n\t}\n\n\ttlItems := make([]list.Item, len(tlFromCache))\n\n\t// this list always get rerendered as it seems to be preferrable over recomputing the string rep of every item in\n\t// every list in m.prTLCache when the terminal window is resized\n\tfor i, result := range tlFromCache {\n\t\ttitle := getPRTLItemTitle(result.item)\n\t\tdescription := getPRTLItemDesc(result.item)\n\n\t\tresult.title = title\n\t\tresult.description = description\n\n\t\ttlItems[i] = result\n\t}\n\n\tm.prTLList.SetItems(tlItems)\n\tm.prTLList.Title = fmt.Sprintf(\"PR #%d Timeline\", prNumber)\n\tm.activePane = prTLListView\n\n\treturn nil, true\n}\n"
  },
  {
    "path": "ui/utils.go",
    "content": "package ui\n\nimport (\n\t\"strings\"\n)\n\nfunc RightPadTrim(s string, length int) string {\n\tif len(s) >= length {\n\t\tif length > 3 {\n\t\t\treturn s[:length-3] + \"...\"\n\t\t}\n\t\treturn s[:length]\n\t}\n\treturn s + strings.Repeat(\" \", length-len(s))\n}\n\nfunc Trim(s string, length int) string {\n\tif len(s) >= length {\n\t\tif length > 3 {\n\t\t\treturn s[:length-3] + \"...\"\n\t\t}\n\t\treturn s[:length]\n\t}\n\treturn s\n}\n\nfunc getFracInt(num int, frac float32) int {\n\treturn int(float32(num) * frac)\n}\n"
  },
  {
    "path": "ui/view.go",
    "content": "package ui\n\nimport (\n\t\"fmt\"\n\n\ttea \"charm.land/bubbletea/v2\"\n\t\"charm.land/lipgloss/v2\"\n)\n\nconst (\n\tviewPortWrapUpperLimit = 160\n\tvpNotReadyMsg          = \"Initializing...\"\n)\n\nfunc (m Model) View() tea.View {\n\tvar content string\n\tvar footer string\n\n\tvar statusBar string\n\tif m.message != \"\" {\n\t\tstatusBar = RightPadTrim(m.message, m.terminalDetails.width)\n\t}\n\n\tswitch m.activePane {\n\tcase prListView:\n\t\tcontent = listStyle.Render(m.prsList.View())\n\tcase prTLListView:\n\t\tcontent = listStyle.Render(m.prTLList.View())\n\tcase repoListView:\n\t\tcontent = listStyle.Render(m.repoList.View())\n\tcase prDetailsView:\n\t\tif !m.prTLItemDetailVPReady {\n\t\t\tcontent = vpNotReadyMsg\n\t\t} else {\n\t\t\tcontent = viewPortStyle.Render(fmt.Sprintf(\"  %s\\n\\n%s\\n\",\n\t\t\t\tprDetailsTitleStyle.Render(m.prDetailsTitle),\n\t\t\t\tm.prDetailsVP.View()))\n\t\t}\n\tcase prTLItemDetailView:\n\t\tvar prRevCmtsVP string\n\t\tif !m.prTLItemDetailVPReady {\n\t\t\tprRevCmtsVP = vpNotReadyMsg\n\t\t} else {\n\t\t\tprRevCmtsVP = viewPortStyle.Render(fmt.Sprintf(\"  %s\\n\\n%s\\n\",\n\t\t\t\thelpVPTitleStyle.Render(m.prTLItemDetailTitle),\n\t\t\t\tm.prTLItemDetailVP.View()))\n\t\t}\n\t\tcontent = prRevCmtsVP\n\tcase helpView:\n\t\tvar helpVP string\n\t\tif !m.helpVPReady {\n\t\t\thelpVP = vpNotReadyMsg\n\t\t} else {\n\t\t\thelpVP = viewPortStyle.Render(fmt.Sprintf(\"  %s\\n\\n%s\\n\",\n\t\t\t\thelpVPTitleStyle.Render(\"Help\"),\n\t\t\t\tm.helpVP.View()))\n\t\t}\n\t\tcontent = helpVP\n\t}\n\n\tvar helpMsg string\n\tif m.showHelp {\n\t\thelpMsg = helpMsgStyle.Render(\"Press ? for help\")\n\t}\n\n\tfooterStr := fmt.Sprintf(\"%s%s\",\n\t\ttoolNameStyle.Render(\"prs\"),\n\t\thelpMsg,\n\t)\n\tfooter = footerStyle.Render(footerStr)\n\n\trendered := lipgloss.JoinVertical(lipgloss.Left,\n\t\tcontent,\n\t\tstatusBar,\n\t\tfooter,\n\t)\n\n\tv := tea.NewView(rendered)\n\tv.AltScreen = true\n\n\treturn v\n}\n"
  },
  {
    "path": "yamlfmt.yml",
    "content": "formatter:\n  retain_line_breaks_single: true\n"
  }
]