├── .commitlintrc.js ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── config.yml ├── actions │ ├── create-check │ │ └── action.yml │ └── install-latest-npm │ │ └── action.yml ├── dependabot.yml ├── matchers │ └── tap.json ├── settings.yml └── workflows │ ├── audit.yml │ ├── ci-release.yml │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── post-dependabot.yml │ ├── pull-request.yml │ ├── release-integration.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .release-please-manifest.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── docs ├── demo.js └── gauge-demo.gif ├── lib ├── base-theme.js ├── error.js ├── has-color.js ├── index.js ├── plumbing.js ├── process.js ├── progress-bar.js ├── render-template.js ├── set-immediate.js ├── set-interval.js ├── spin.js ├── template-item.js ├── theme-set.js ├── themes.js └── wide-truncate.js ├── package.json ├── release-please-config.json └── test ├── base-theme.js ├── error.js ├── index.js ├── plumbing.js ├── progress-bar.js ├── render-template.js ├── spin.js ├── template-item.js ├── theme-set.js ├── themes.js └── wide-truncate.js /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | /* This file is automatically added by @npmcli/template-oss. Do not edit. */ 2 | 3 | module.exports = { 4 | extends: ['@commitlint/config-conventional'], 5 | rules: { 6 | 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'deps', 'chore']], 7 | 'header-max-length': [2, 'always', 80], 8 | 'subject-case': [0], 9 | 'body-max-line-length': [0], 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* This file is automatically added by @npmcli/template-oss. Do not edit. */ 2 | 3 | 'use strict' 4 | 5 | const { readdirSync: readdir } = require('fs') 6 | 7 | const localConfigs = readdir(__dirname) 8 | .filter((file) => file.startsWith('.eslintrc.local.')) 9 | .map((file) => `./${file}`) 10 | 11 | module.exports = { 12 | root: true, 13 | ignorePatterns: [ 14 | 'tap-testdir*/', 15 | ], 16 | extends: [ 17 | '@npmcli', 18 | ...localConfigs, 19 | ], 20 | } 21 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | * @npm/cli-team 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Bug 4 | description: File a bug/issue 5 | title: "[BUG] " 6 | labels: [ Bug, Needs Triage ] 7 | 8 | body: 9 | - type: checkboxes 10 | attributes: 11 | label: Is there an existing issue for this? 12 | description: Please [search here](./issues) to see if an issue already exists for your problem. 13 | options: 14 | - label: I have searched the existing issues 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: Current Behavior 19 | description: A clear & concise description of what you're experiencing. 20 | validations: 21 | required: false 22 | - type: textarea 23 | attributes: 24 | label: Expected Behavior 25 | description: A clear & concise description of what you expected to happen. 26 | validations: 27 | required: false 28 | - type: textarea 29 | attributes: 30 | label: Steps To Reproduce 31 | description: Steps to reproduce the behavior. 32 | value: | 33 | 1. In this environment... 34 | 2. With this config... 35 | 3. Run '...' 36 | 4. See error... 37 | validations: 38 | required: false 39 | - type: textarea 40 | attributes: 41 | label: Environment 42 | description: | 43 | examples: 44 | - **npm**: 7.6.3 45 | - **Node**: 13.14.0 46 | - **OS**: Ubuntu 20.04 47 | - **platform**: Macbook Pro 48 | value: | 49 | - npm: 50 | - Node: 51 | - OS: 52 | - platform: 53 | validations: 54 | required: false 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | blank_issues_enabled: true 4 | -------------------------------------------------------------------------------- /.github/actions/create-check/action.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: 'Create Check' 4 | inputs: 5 | name: 6 | required: true 7 | token: 8 | required: true 9 | sha: 10 | required: true 11 | check-name: 12 | default: '' 13 | outputs: 14 | check-id: 15 | value: ${{ steps.create-check.outputs.check_id }} 16 | runs: 17 | using: "composite" 18 | steps: 19 | - name: Get Workflow Job 20 | uses: actions/github-script@v7 21 | id: workflow 22 | env: 23 | JOB_NAME: "${{ inputs.name }}" 24 | SHA: "${{ inputs.sha }}" 25 | with: 26 | result-encoding: string 27 | script: | 28 | const { repo: { owner, repo}, runId, serverUrl } = context 29 | const { JOB_NAME, SHA } = process.env 30 | 31 | const job = await github.rest.actions.listJobsForWorkflowRun({ 32 | owner, 33 | repo, 34 | run_id: runId, 35 | per_page: 100 36 | }).then(r => r.data.jobs.find(j => j.name.endsWith(JOB_NAME))) 37 | 38 | return [ 39 | `This check is assosciated with ${serverUrl}/${owner}/${repo}/commit/${SHA}.`, 40 | 'Run logs:', 41 | job?.html_url || `could not be found for a job ending with: "${JOB_NAME}"`, 42 | ].join(' ') 43 | - name: Create Check 44 | uses: LouisBrunner/checks-action@v1.6.0 45 | id: create-check 46 | with: 47 | token: ${{ inputs.token }} 48 | sha: ${{ inputs.sha }} 49 | status: in_progress 50 | name: ${{ inputs.check-name || inputs.name }} 51 | output: | 52 | {"summary":"${{ steps.workflow.outputs.result }}"} 53 | -------------------------------------------------------------------------------- /.github/actions/install-latest-npm/action.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: 'Install Latest npm' 4 | description: 'Install the latest version of npm compatible with the Node version' 5 | inputs: 6 | node: 7 | description: 'Current Node version' 8 | required: true 9 | runs: 10 | using: "composite" 11 | steps: 12 | # node 10/12/14 ship with npm@6, which is known to fail when updating itself in windows 13 | - name: Update Windows npm 14 | if: | 15 | runner.os == 'Windows' && ( 16 | startsWith(inputs.node, 'v10.') || 17 | startsWith(inputs.node, 'v12.') || 18 | startsWith(inputs.node, 'v14.') 19 | ) 20 | shell: cmd 21 | run: | 22 | curl -sO https://registry.npmjs.org/npm/-/npm-7.5.4.tgz 23 | tar xf npm-7.5.4.tgz 24 | cd package 25 | node lib/npm.js install --no-fund --no-audit -g ..\npm-7.5.4.tgz 26 | cd .. 27 | rmdir /s /q package 28 | - name: Install Latest npm 29 | shell: bash 30 | env: 31 | NODE_VERSION: ${{ inputs.node }} 32 | working-directory: ${{ runner.temp }} 33 | run: | 34 | MATCH="" 35 | SPECS=("latest" "next-10" "next-9" "next-8" "next-7" "next-6") 36 | 37 | echo "node@$NODE_VERSION" 38 | 39 | for SPEC in ${SPECS[@]}; do 40 | ENGINES=$(npm view npm@$SPEC --json | jq -r '.engines.node') 41 | echo "Checking if node@$NODE_VERSION satisfies npm@$SPEC ($ENGINES)" 42 | 43 | if npx semver -r "$ENGINES" "$NODE_VERSION" > /dev/null; then 44 | MATCH=$SPEC 45 | echo "Found compatible version: npm@$MATCH" 46 | break 47 | fi 48 | done 49 | 50 | if [ -z $MATCH ]; then 51 | echo "Could not find a compatible version of npm for node@$NODE_VERSION" 52 | exit 1 53 | fi 54 | 55 | npm i --prefer-online --no-fund --no-audit -g npm@$MATCH 56 | - name: npm Version 57 | shell: bash 58 | run: npm -v 59 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | version: 2 4 | 5 | updates: 6 | - package-ecosystem: npm 7 | directory: / 8 | schedule: 9 | interval: daily 10 | target-branch: "main" 11 | allow: 12 | - dependency-type: direct 13 | versioning-strategy: increase-if-necessary 14 | commit-message: 15 | prefix: deps 16 | prefix-development: chore 17 | labels: 18 | - "Dependencies" 19 | open-pull-requests-limit: 10 20 | -------------------------------------------------------------------------------- /.github/matchers/tap.json: -------------------------------------------------------------------------------- 1 | { 2 | "//@npmcli/template-oss": "This file is automatically added by @npmcli/template-oss. Do not edit.", 3 | "problemMatcher": [ 4 | { 5 | "owner": "tap", 6 | "pattern": [ 7 | { 8 | "regexp": "^\\s*not ok \\d+ - (.*)", 9 | "message": 1 10 | }, 11 | { 12 | "regexp": "^\\s*---" 13 | }, 14 | { 15 | "regexp": "^\\s*at:" 16 | }, 17 | { 18 | "regexp": "^\\s*line:\\s*(\\d+)", 19 | "line": 1 20 | }, 21 | { 22 | "regexp": "^\\s*column:\\s*(\\d+)", 23 | "column": 1 24 | }, 25 | { 26 | "regexp": "^\\s*file:\\s*(.*)", 27 | "file": 1 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | repository: 4 | allow_merge_commit: false 5 | allow_rebase_merge: true 6 | allow_squash_merge: true 7 | squash_merge_commit_title: PR_TITLE 8 | squash_merge_commit_message: PR_BODY 9 | delete_branch_on_merge: true 10 | enable_automated_security_fixes: true 11 | enable_vulnerability_alerts: true 12 | 13 | branches: 14 | - name: main 15 | protection: 16 | required_status_checks: null 17 | enforce_admins: true 18 | block_creations: true 19 | required_pull_request_reviews: 20 | required_approving_review_count: 1 21 | require_code_owner_reviews: true 22 | require_last_push_approval: true 23 | dismiss_stale_reviews: true 24 | restrictions: 25 | apps: [] 26 | users: [] 27 | teams: [ "cli-team" ] 28 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Audit 4 | 5 | on: 6 | workflow_dispatch: 7 | schedule: 8 | # "At 08:00 UTC (01:00 PT) on Monday" https://crontab.guru/#0_8_*_*_1 9 | - cron: "0 8 * * 1" 10 | 11 | jobs: 12 | audit: 13 | name: Audit Dependencies 14 | if: github.repository_owner == 'npm' 15 | runs-on: ubuntu-latest 16 | defaults: 17 | run: 18 | shell: bash 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | - name: Setup Git User 23 | run: | 24 | git config --global user.email "npm-cli+bot@github.com" 25 | git config --global user.name "npm CLI robot" 26 | - name: Setup Node 27 | uses: actions/setup-node@v4 28 | id: node 29 | with: 30 | node-version: 22.x 31 | check-latest: contains('22.x', '.x') 32 | - name: Install Latest npm 33 | uses: ./.github/actions/install-latest-npm 34 | with: 35 | node: ${{ steps.node.outputs.node-version }} 36 | - name: Install Dependencies 37 | run: npm i --ignore-scripts --no-audit --no-fund --package-lock 38 | - name: Run Production Audit 39 | run: npm audit --omit=dev 40 | - name: Run Full Audit 41 | run: npm audit --audit-level=none 42 | -------------------------------------------------------------------------------- /.github/workflows/ci-release.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: CI - Release 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | ref: 9 | required: true 10 | type: string 11 | default: main 12 | workflow_call: 13 | inputs: 14 | ref: 15 | required: true 16 | type: string 17 | check-sha: 18 | required: true 19 | type: string 20 | 21 | jobs: 22 | lint-all: 23 | name: Lint All 24 | if: github.repository_owner == 'npm' 25 | runs-on: ubuntu-latest 26 | defaults: 27 | run: 28 | shell: bash 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | with: 33 | ref: ${{ inputs.ref }} 34 | - name: Setup Git User 35 | run: | 36 | git config --global user.email "npm-cli+bot@github.com" 37 | git config --global user.name "npm CLI robot" 38 | - name: Create Check 39 | id: create-check 40 | if: ${{ inputs.check-sha }} 41 | uses: ./.github/actions/create-check 42 | with: 43 | name: "Lint All" 44 | token: ${{ secrets.GITHUB_TOKEN }} 45 | sha: ${{ inputs.check-sha }} 46 | - name: Setup Node 47 | uses: actions/setup-node@v4 48 | id: node 49 | with: 50 | node-version: 22.x 51 | check-latest: contains('22.x', '.x') 52 | - name: Install Latest npm 53 | uses: ./.github/actions/install-latest-npm 54 | with: 55 | node: ${{ steps.node.outputs.node-version }} 56 | - name: Install Dependencies 57 | run: npm i --ignore-scripts --no-audit --no-fund 58 | - name: Lint 59 | run: npm run lint --ignore-scripts 60 | - name: Post Lint 61 | run: npm run postlint --ignore-scripts 62 | - name: Conclude Check 63 | uses: LouisBrunner/checks-action@v1.6.0 64 | if: always() 65 | with: 66 | token: ${{ secrets.GITHUB_TOKEN }} 67 | conclusion: ${{ job.status }} 68 | check_id: ${{ steps.create-check.outputs.check-id }} 69 | 70 | test-all: 71 | name: Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }} 72 | if: github.repository_owner == 'npm' 73 | strategy: 74 | fail-fast: false 75 | matrix: 76 | platform: 77 | - name: Linux 78 | os: ubuntu-latest 79 | shell: bash 80 | - name: macOS 81 | os: macos-latest 82 | shell: bash 83 | - name: macOS 84 | os: macos-13 85 | shell: bash 86 | - name: Windows 87 | os: windows-latest 88 | shell: cmd 89 | node-version: 90 | - 14.17.0 91 | - 14.x 92 | - 16.13.0 93 | - 16.x 94 | - 18.0.0 95 | - 18.x 96 | - 20.x 97 | - 22.x 98 | exclude: 99 | - platform: { name: macOS, os: macos-latest, shell: bash } 100 | node-version: 14.17.0 101 | - platform: { name: macOS, os: macos-latest, shell: bash } 102 | node-version: 14.x 103 | - platform: { name: macOS, os: macos-13, shell: bash } 104 | node-version: 16.13.0 105 | - platform: { name: macOS, os: macos-13, shell: bash } 106 | node-version: 16.x 107 | - platform: { name: macOS, os: macos-13, shell: bash } 108 | node-version: 18.0.0 109 | - platform: { name: macOS, os: macos-13, shell: bash } 110 | node-version: 18.x 111 | - platform: { name: macOS, os: macos-13, shell: bash } 112 | node-version: 20.x 113 | - platform: { name: macOS, os: macos-13, shell: bash } 114 | node-version: 22.x 115 | runs-on: ${{ matrix.platform.os }} 116 | defaults: 117 | run: 118 | shell: ${{ matrix.platform.shell }} 119 | steps: 120 | - name: Checkout 121 | uses: actions/checkout@v4 122 | with: 123 | ref: ${{ inputs.ref }} 124 | - name: Setup Git User 125 | run: | 126 | git config --global user.email "npm-cli+bot@github.com" 127 | git config --global user.name "npm CLI robot" 128 | - name: Create Check 129 | id: create-check 130 | if: ${{ inputs.check-sha }} 131 | uses: ./.github/actions/create-check 132 | with: 133 | name: "Test All - ${{ matrix.platform.name }} - ${{ matrix.node-version }}" 134 | token: ${{ secrets.GITHUB_TOKEN }} 135 | sha: ${{ inputs.check-sha }} 136 | - name: Setup Node 137 | uses: actions/setup-node@v4 138 | id: node 139 | with: 140 | node-version: ${{ matrix.node-version }} 141 | check-latest: contains(matrix.node-version, '.x') 142 | - name: Install Latest npm 143 | uses: ./.github/actions/install-latest-npm 144 | with: 145 | node: ${{ steps.node.outputs.node-version }} 146 | - name: Install Dependencies 147 | run: npm i --ignore-scripts --no-audit --no-fund 148 | - name: Add Problem Matcher 149 | run: echo "::add-matcher::.github/matchers/tap.json" 150 | - name: Test 151 | run: npm test --ignore-scripts 152 | - name: Conclude Check 153 | uses: LouisBrunner/checks-action@v1.6.0 154 | if: always() 155 | with: 156 | token: ${{ secrets.GITHUB_TOKEN }} 157 | conclusion: ${{ job.status }} 158 | check_id: ${{ steps.create-check.outputs.check-id }} 159 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: CI 4 | 5 | on: 6 | workflow_dispatch: 7 | pull_request: 8 | push: 9 | branches: 10 | - main 11 | schedule: 12 | # "At 09:00 UTC (02:00 PT) on Monday" https://crontab.guru/#0_9_*_*_1 13 | - cron: "0 9 * * 1" 14 | 15 | jobs: 16 | lint: 17 | name: Lint 18 | if: github.repository_owner == 'npm' 19 | runs-on: ubuntu-latest 20 | defaults: 21 | run: 22 | shell: bash 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | - name: Setup Git User 27 | run: | 28 | git config --global user.email "npm-cli+bot@github.com" 29 | git config --global user.name "npm CLI robot" 30 | - name: Setup Node 31 | uses: actions/setup-node@v4 32 | id: node 33 | with: 34 | node-version: 22.x 35 | check-latest: contains('22.x', '.x') 36 | - name: Install Latest npm 37 | uses: ./.github/actions/install-latest-npm 38 | with: 39 | node: ${{ steps.node.outputs.node-version }} 40 | - name: Install Dependencies 41 | run: npm i --ignore-scripts --no-audit --no-fund 42 | - name: Lint 43 | run: npm run lint --ignore-scripts 44 | - name: Post Lint 45 | run: npm run postlint --ignore-scripts 46 | 47 | test: 48 | name: Test - ${{ matrix.platform.name }} - ${{ matrix.node-version }} 49 | if: github.repository_owner == 'npm' 50 | strategy: 51 | fail-fast: false 52 | matrix: 53 | platform: 54 | - name: Linux 55 | os: ubuntu-latest 56 | shell: bash 57 | - name: macOS 58 | os: macos-latest 59 | shell: bash 60 | - name: macOS 61 | os: macos-13 62 | shell: bash 63 | - name: Windows 64 | os: windows-latest 65 | shell: cmd 66 | node-version: 67 | - 14.17.0 68 | - 14.x 69 | - 16.13.0 70 | - 16.x 71 | - 18.0.0 72 | - 18.x 73 | - 20.x 74 | - 22.x 75 | exclude: 76 | - platform: { name: macOS, os: macos-latest, shell: bash } 77 | node-version: 14.17.0 78 | - platform: { name: macOS, os: macos-latest, shell: bash } 79 | node-version: 14.x 80 | - platform: { name: macOS, os: macos-13, shell: bash } 81 | node-version: 16.13.0 82 | - platform: { name: macOS, os: macos-13, shell: bash } 83 | node-version: 16.x 84 | - platform: { name: macOS, os: macos-13, shell: bash } 85 | node-version: 18.0.0 86 | - platform: { name: macOS, os: macos-13, shell: bash } 87 | node-version: 18.x 88 | - platform: { name: macOS, os: macos-13, shell: bash } 89 | node-version: 20.x 90 | - platform: { name: macOS, os: macos-13, shell: bash } 91 | node-version: 22.x 92 | runs-on: ${{ matrix.platform.os }} 93 | defaults: 94 | run: 95 | shell: ${{ matrix.platform.shell }} 96 | steps: 97 | - name: Checkout 98 | uses: actions/checkout@v4 99 | - name: Setup Git User 100 | run: | 101 | git config --global user.email "npm-cli+bot@github.com" 102 | git config --global user.name "npm CLI robot" 103 | - name: Setup Node 104 | uses: actions/setup-node@v4 105 | id: node 106 | with: 107 | node-version: ${{ matrix.node-version }} 108 | check-latest: contains(matrix.node-version, '.x') 109 | - name: Install Latest npm 110 | uses: ./.github/actions/install-latest-npm 111 | with: 112 | node: ${{ steps.node.outputs.node-version }} 113 | - name: Install Dependencies 114 | run: npm i --ignore-scripts --no-audit --no-fund 115 | - name: Add Problem Matcher 116 | run: echo "::add-matcher::.github/matchers/tap.json" 117 | - name: Test 118 | run: npm test --ignore-scripts 119 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: CodeQL 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | branches: 11 | - main 12 | schedule: 13 | # "At 10:00 UTC (03:00 PT) on Monday" https://crontab.guru/#0_10_*_*_1 14 | - cron: "0 10 * * 1" 15 | 16 | jobs: 17 | analyze: 18 | name: Analyze 19 | runs-on: ubuntu-latest 20 | permissions: 21 | actions: read 22 | contents: read 23 | security-events: write 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | - name: Setup Git User 28 | run: | 29 | git config --global user.email "npm-cli+bot@github.com" 30 | git config --global user.name "npm CLI robot" 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v2 33 | with: 34 | languages: javascript 35 | - name: Perform CodeQL Analysis 36 | uses: github/codeql-action/analyze@v2 37 | -------------------------------------------------------------------------------- /.github/workflows/post-dependabot.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Post Dependabot 4 | 5 | on: pull_request 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | template-oss: 12 | name: template-oss 13 | if: github.repository_owner == 'npm' && github.actor == 'dependabot[bot]' 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | shell: bash 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | ref: ${{ github.event.pull_request.head.ref }} 23 | - name: Setup Git User 24 | run: | 25 | git config --global user.email "npm-cli+bot@github.com" 26 | git config --global user.name "npm CLI robot" 27 | - name: Setup Node 28 | uses: actions/setup-node@v4 29 | id: node 30 | with: 31 | node-version: 22.x 32 | check-latest: contains('22.x', '.x') 33 | - name: Install Latest npm 34 | uses: ./.github/actions/install-latest-npm 35 | with: 36 | node: ${{ steps.node.outputs.node-version }} 37 | - name: Install Dependencies 38 | run: npm i --ignore-scripts --no-audit --no-fund 39 | - name: Fetch Dependabot Metadata 40 | id: metadata 41 | uses: dependabot/fetch-metadata@v1 42 | with: 43 | github-token: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | # Dependabot can update multiple directories so we output which directory 46 | # it is acting on so we can run the command for the correct root or workspace 47 | - name: Get Dependabot Directory 48 | if: contains(steps.metadata.outputs.dependency-names, '@npmcli/template-oss') 49 | id: flags 50 | run: | 51 | dependabot_dir="${{ steps.metadata.outputs.directory }}" 52 | if [[ "$dependabot_dir" == "/" ]]; then 53 | echo "workspace=-iwr" >> $GITHUB_OUTPUT 54 | else 55 | # strip leading slash from directory so it works as a 56 | # a path to the workspace flag 57 | echo "workspace=-w ${dependabot_dir#/}" >> $GITHUB_OUTPUT 58 | fi 59 | 60 | - name: Apply Changes 61 | if: steps.flags.outputs.workspace 62 | id: apply 63 | run: | 64 | npm run template-oss-apply ${{ steps.flags.outputs.workspace }} 65 | if [[ `git status --porcelain` ]]; then 66 | echo "changes=true" >> $GITHUB_OUTPUT 67 | fi 68 | # This only sets the conventional commit prefix. This workflow can't reliably determine 69 | # what the breaking change is though. If a BREAKING CHANGE message is required then 70 | # this PR check will fail and the commit will be amended with stafftools 71 | if [[ "${{ steps.metadata.outputs.update-type }}" == "version-update:semver-major" ]]; then 72 | prefix='feat!' 73 | else 74 | prefix='chore' 75 | fi 76 | echo "message=$prefix: postinstall for dependabot template-oss PR" >> $GITHUB_OUTPUT 77 | 78 | # This step will fail if template-oss has made any workflow updates. It is impossible 79 | # for a workflow to update other workflows. In the case it does fail, we continue 80 | # and then try to apply only a portion of the changes in the next step 81 | - name: Push All Changes 82 | if: steps.apply.outputs.changes 83 | id: push 84 | continue-on-error: true 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | run: | 88 | git commit -am "${{ steps.apply.outputs.message }}" 89 | git push 90 | 91 | # If the previous step failed, then reset the commit and remove any workflow changes 92 | # and attempt to commit and push again. This is helpful because we will have a commit 93 | # with the correct prefix that we can then --amend with @npmcli/stafftools later. 94 | - name: Push All Changes Except Workflows 95 | if: steps.apply.outputs.changes && steps.push.outcome == 'failure' 96 | env: 97 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 98 | run: | 99 | git reset HEAD~ 100 | git checkout HEAD -- .github/workflows/ 101 | git clean -fd .github/workflows/ 102 | git commit -am "${{ steps.apply.outputs.message }}" 103 | git push 104 | 105 | # Check if all the necessary template-oss changes were applied. Since we continued 106 | # on errors in one of the previous steps, this check will fail if our follow up 107 | # only applied a portion of the changes and we need to followup manually. 108 | # 109 | # Note that this used to run `lint` and `postlint` but that will fail this action 110 | # if we've also shipped any linting changes separate from template-oss. We do 111 | # linting in another action, so we want to fail this one only if there are 112 | # template-oss changes that could not be applied. 113 | - name: Check Changes 114 | if: steps.apply.outputs.changes 115 | run: | 116 | npm exec --offline ${{ steps.flags.outputs.workspace }} -- template-oss-check 117 | 118 | - name: Fail on Breaking Change 119 | if: steps.apply.outputs.changes && startsWith(steps.apply.outputs.message, 'feat!') 120 | run: | 121 | echo "This PR has a breaking change. Run 'npx -p @npmcli/stafftools gh template-oss-fix'" 122 | echo "for more information on how to fix this with a BREAKING CHANGE footer." 123 | exit 1 124 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Pull Request 4 | 5 | on: 6 | pull_request: 7 | types: 8 | - opened 9 | - reopened 10 | - edited 11 | - synchronize 12 | 13 | jobs: 14 | commitlint: 15 | name: Lint Commits 16 | if: github.repository_owner == 'npm' 17 | runs-on: ubuntu-latest 18 | defaults: 19 | run: 20 | shell: bash 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | - name: Setup Git User 27 | run: | 28 | git config --global user.email "npm-cli+bot@github.com" 29 | git config --global user.name "npm CLI robot" 30 | - name: Setup Node 31 | uses: actions/setup-node@v4 32 | id: node 33 | with: 34 | node-version: 22.x 35 | check-latest: contains('22.x', '.x') 36 | - name: Install Latest npm 37 | uses: ./.github/actions/install-latest-npm 38 | with: 39 | node: ${{ steps.node.outputs.node-version }} 40 | - name: Install Dependencies 41 | run: npm i --ignore-scripts --no-audit --no-fund 42 | - name: Run Commitlint on Commits 43 | id: commit 44 | continue-on-error: true 45 | run: npx --offline commitlint -V --from 'origin/${{ github.base_ref }}' --to ${{ github.event.pull_request.head.sha }} 46 | - name: Run Commitlint on PR Title 47 | if: steps.commit.outcome == 'failure' 48 | env: 49 | PR_TITLE: ${{ github.event.pull_request.title }} 50 | run: echo "$PR_TITLE" | npx --offline commitlint -V 51 | -------------------------------------------------------------------------------- /.github/workflows/release-integration.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Release Integration 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | releases: 9 | required: true 10 | type: string 11 | description: 'A json array of releases. Required fields: publish: tagName, publishTag. publish check: pkgName, version' 12 | workflow_call: 13 | inputs: 14 | releases: 15 | required: true 16 | type: string 17 | description: 'A json array of releases. Required fields: publish: tagName, publishTag. publish check: pkgName, version' 18 | secrets: 19 | PUBLISH_TOKEN: 20 | required: true 21 | 22 | jobs: 23 | publish: 24 | name: Publish 25 | runs-on: ubuntu-latest 26 | defaults: 27 | run: 28 | shell: bash 29 | permissions: 30 | id-token: write 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | with: 35 | ref: ${{ fromJSON(inputs.releases)[0].tagName }} 36 | - name: Setup Git User 37 | run: | 38 | git config --global user.email "npm-cli+bot@github.com" 39 | git config --global user.name "npm CLI robot" 40 | - name: Setup Node 41 | uses: actions/setup-node@v4 42 | id: node 43 | with: 44 | node-version: 22.x 45 | check-latest: contains('22.x', '.x') 46 | - name: Install Latest npm 47 | uses: ./.github/actions/install-latest-npm 48 | with: 49 | node: ${{ steps.node.outputs.node-version }} 50 | - name: Install Dependencies 51 | run: npm i --ignore-scripts --no-audit --no-fund 52 | - name: Set npm authToken 53 | run: npm config set '//registry.npmjs.org/:_authToken'=\${PUBLISH_TOKEN} 54 | - name: Publish 55 | env: 56 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 57 | RELEASES: ${{ inputs.releases }} 58 | run: | 59 | EXIT_CODE=0 60 | 61 | for release in $(echo $RELEASES | jq -r '.[] | @base64'); do 62 | PUBLISH_TAG=$(echo "$release" | base64 --decode | jq -r .publishTag) 63 | npm publish --provenance --tag="$PUBLISH_TAG" 64 | STATUS=$? 65 | if [[ "$STATUS" -eq 1 ]]; then 66 | EXIT_CODE=$STATUS 67 | fi 68 | done 69 | 70 | exit $EXIT_CODE 71 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | name: Release 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | checks: write 14 | 15 | jobs: 16 | release: 17 | outputs: 18 | pr: ${{ steps.release.outputs.pr }} 19 | pr-branch: ${{ steps.release.outputs.pr-branch }} 20 | pr-number: ${{ steps.release.outputs.pr-number }} 21 | pr-sha: ${{ steps.release.outputs.pr-sha }} 22 | releases: ${{ steps.release.outputs.releases }} 23 | comment-id: ${{ steps.create-comment.outputs.comment-id || steps.update-comment.outputs.comment-id }} 24 | check-id: ${{ steps.create-check.outputs.check-id }} 25 | name: Release 26 | if: github.repository_owner == 'npm' 27 | runs-on: ubuntu-latest 28 | defaults: 29 | run: 30 | shell: bash 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Git User 35 | run: | 36 | git config --global user.email "npm-cli+bot@github.com" 37 | git config --global user.name "npm CLI robot" 38 | - name: Setup Node 39 | uses: actions/setup-node@v4 40 | id: node 41 | with: 42 | node-version: 22.x 43 | check-latest: contains('22.x', '.x') 44 | - name: Install Latest npm 45 | uses: ./.github/actions/install-latest-npm 46 | with: 47 | node: ${{ steps.node.outputs.node-version }} 48 | - name: Install Dependencies 49 | run: npm i --ignore-scripts --no-audit --no-fund 50 | - name: Release Please 51 | id: release 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | run: npx --offline template-oss-release-please --branch="${{ github.ref_name }}" --backport="" --defaultTag="latest" 55 | - name: Create Release Manager Comment Text 56 | if: steps.release.outputs.pr-number 57 | uses: actions/github-script@v7 58 | id: comment-text 59 | with: 60 | result-encoding: string 61 | script: | 62 | const { runId, repo: { owner, repo } } = context 63 | const { data: workflow } = await github.rest.actions.getWorkflowRun({ owner, repo, run_id: runId }) 64 | return['## Release Manager', `Release workflow run: ${workflow.html_url}`].join('\n\n') 65 | - name: Find Release Manager Comment 66 | uses: peter-evans/find-comment@v2 67 | if: steps.release.outputs.pr-number 68 | id: found-comment 69 | with: 70 | issue-number: ${{ steps.release.outputs.pr-number }} 71 | comment-author: 'github-actions[bot]' 72 | body-includes: '## Release Manager' 73 | - name: Create Release Manager Comment 74 | id: create-comment 75 | if: steps.release.outputs.pr-number && !steps.found-comment.outputs.comment-id 76 | uses: peter-evans/create-or-update-comment@v3 77 | with: 78 | issue-number: ${{ steps.release.outputs.pr-number }} 79 | body: ${{ steps.comment-text.outputs.result }} 80 | - name: Update Release Manager Comment 81 | id: update-comment 82 | if: steps.release.outputs.pr-number && steps.found-comment.outputs.comment-id 83 | uses: peter-evans/create-or-update-comment@v3 84 | with: 85 | comment-id: ${{ steps.found-comment.outputs.comment-id }} 86 | body: ${{ steps.comment-text.outputs.result }} 87 | edit-mode: 'replace' 88 | - name: Create Check 89 | id: create-check 90 | uses: ./.github/actions/create-check 91 | if: steps.release.outputs.pr-sha 92 | with: 93 | name: "Release" 94 | token: ${{ secrets.GITHUB_TOKEN }} 95 | sha: ${{ steps.release.outputs.pr-sha }} 96 | 97 | update: 98 | needs: release 99 | outputs: 100 | sha: ${{ steps.commit.outputs.sha }} 101 | check-id: ${{ steps.create-check.outputs.check-id }} 102 | name: Update - Release 103 | if: github.repository_owner == 'npm' && needs.release.outputs.pr 104 | runs-on: ubuntu-latest 105 | defaults: 106 | run: 107 | shell: bash 108 | steps: 109 | - name: Checkout 110 | uses: actions/checkout@v4 111 | with: 112 | fetch-depth: 0 113 | ref: ${{ needs.release.outputs.pr-branch }} 114 | - name: Setup Git User 115 | run: | 116 | git config --global user.email "npm-cli+bot@github.com" 117 | git config --global user.name "npm CLI robot" 118 | - name: Setup Node 119 | uses: actions/setup-node@v4 120 | id: node 121 | with: 122 | node-version: 22.x 123 | check-latest: contains('22.x', '.x') 124 | - name: Install Latest npm 125 | uses: ./.github/actions/install-latest-npm 126 | with: 127 | node: ${{ steps.node.outputs.node-version }} 128 | - name: Install Dependencies 129 | run: npm i --ignore-scripts --no-audit --no-fund 130 | - name: Create Release Manager Checklist Text 131 | id: comment-text 132 | env: 133 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 134 | run: npm exec --offline -- template-oss-release-manager --pr="${{ needs.release.outputs.pr-number }}" --backport="" --defaultTag="latest" --publish 135 | - name: Append Release Manager Comment 136 | uses: peter-evans/create-or-update-comment@v3 137 | with: 138 | comment-id: ${{ needs.release.outputs.comment-id }} 139 | body: ${{ steps.comment-text.outputs.result }} 140 | edit-mode: 'append' 141 | - name: Run Post Pull Request Actions 142 | env: 143 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 144 | run: npm run rp-pull-request --ignore-scripts --if-present -- --pr="${{ needs.release.outputs.pr-number }}" --commentId="${{ needs.release.outputs.comment-id }}" 145 | - name: Commit 146 | id: commit 147 | env: 148 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 149 | run: | 150 | git commit --all --amend --no-edit || true 151 | git push --force-with-lease 152 | echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 153 | - name: Create Check 154 | id: create-check 155 | uses: ./.github/actions/create-check 156 | with: 157 | name: "Update - Release" 158 | check-name: "Release" 159 | token: ${{ secrets.GITHUB_TOKEN }} 160 | sha: ${{ steps.commit.outputs.sha }} 161 | - name: Conclude Check 162 | uses: LouisBrunner/checks-action@v1.6.0 163 | with: 164 | token: ${{ secrets.GITHUB_TOKEN }} 165 | conclusion: ${{ job.status }} 166 | check_id: ${{ needs.release.outputs.check-id }} 167 | 168 | ci: 169 | name: CI - Release 170 | needs: [ release, update ] 171 | if: needs.release.outputs.pr 172 | uses: ./.github/workflows/ci-release.yml 173 | with: 174 | ref: ${{ needs.release.outputs.pr-branch }} 175 | check-sha: ${{ needs.update.outputs.sha }} 176 | 177 | post-ci: 178 | needs: [ release, update, ci ] 179 | name: Post CI - Release 180 | if: github.repository_owner == 'npm' && needs.release.outputs.pr && always() 181 | runs-on: ubuntu-latest 182 | defaults: 183 | run: 184 | shell: bash 185 | steps: 186 | - name: Get CI Conclusion 187 | id: conclusion 188 | run: | 189 | result="" 190 | if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then 191 | result="failure" 192 | elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then 193 | result="cancelled" 194 | else 195 | result="success" 196 | fi 197 | echo "result=$result" >> $GITHUB_OUTPUT 198 | - name: Conclude Check 199 | uses: LouisBrunner/checks-action@v1.6.0 200 | with: 201 | token: ${{ secrets.GITHUB_TOKEN }} 202 | conclusion: ${{ steps.conclusion.outputs.result }} 203 | check_id: ${{ needs.update.outputs.check-id }} 204 | 205 | post-release: 206 | needs: release 207 | outputs: 208 | comment-id: ${{ steps.create-comment.outputs.comment-id }} 209 | name: Post Release - Release 210 | if: github.repository_owner == 'npm' && needs.release.outputs.releases 211 | runs-on: ubuntu-latest 212 | defaults: 213 | run: 214 | shell: bash 215 | steps: 216 | - name: Create Release PR Comment Text 217 | id: comment-text 218 | uses: actions/github-script@v7 219 | env: 220 | RELEASES: ${{ needs.release.outputs.releases }} 221 | with: 222 | result-encoding: string 223 | script: | 224 | const releases = JSON.parse(process.env.RELEASES) 225 | const { runId, repo: { owner, repo } } = context 226 | const issue_number = releases[0].prNumber 227 | const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}` 228 | 229 | return [ 230 | '## Release Workflow\n', 231 | ...releases.map(r => `- \`${r.pkgName}@${r.version}\` ${r.url}`), 232 | `- Workflow run: :arrows_counterclockwise: ${runUrl}`, 233 | ].join('\n') 234 | - name: Create Release PR Comment 235 | id: create-comment 236 | uses: peter-evans/create-or-update-comment@v3 237 | with: 238 | issue-number: ${{ fromJSON(needs.release.outputs.releases)[0].prNumber }} 239 | body: ${{ steps.comment-text.outputs.result }} 240 | 241 | release-integration: 242 | needs: release 243 | name: Release Integration 244 | if: needs.release.outputs.releases 245 | uses: ./.github/workflows/release-integration.yml 246 | permissions: 247 | id-token: write 248 | secrets: 249 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 250 | with: 251 | releases: ${{ needs.release.outputs.releases }} 252 | 253 | post-release-integration: 254 | needs: [ release, release-integration, post-release ] 255 | name: Post Release Integration - Release 256 | if: github.repository_owner == 'npm' && needs.release.outputs.releases && always() 257 | runs-on: ubuntu-latest 258 | defaults: 259 | run: 260 | shell: bash 261 | steps: 262 | - name: Get Post Release Conclusion 263 | id: conclusion 264 | run: | 265 | if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then 266 | result="x" 267 | elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then 268 | result="heavy_multiplication_x" 269 | else 270 | result="white_check_mark" 271 | fi 272 | echo "result=$result" >> $GITHUB_OUTPUT 273 | - name: Find Release PR Comment 274 | uses: peter-evans/find-comment@v2 275 | id: found-comment 276 | with: 277 | issue-number: ${{ fromJSON(needs.release.outputs.releases)[0].prNumber }} 278 | comment-author: 'github-actions[bot]' 279 | body-includes: '## Release Workflow' 280 | - name: Create Release PR Comment Text 281 | id: comment-text 282 | if: steps.found-comment.outputs.comment-id 283 | uses: actions/github-script@v7 284 | env: 285 | RESULT: ${{ steps.conclusion.outputs.result }} 286 | BODY: ${{ steps.found-comment.outputs.comment-body }} 287 | with: 288 | result-encoding: string 289 | script: | 290 | const { RESULT, BODY } = process.env 291 | const body = [BODY.replace(/(Workflow run: :)[a-z_]+(:)/, `$1${RESULT}$2`)] 292 | if (RESULT !== 'white_check_mark') { 293 | body.push(':rotating_light::rotating_light::rotating_light:') 294 | body.push([ 295 | '@npm/cli-team: The post-release workflow failed for this release.', 296 | 'Manual steps may need to be taken after examining the workflow output.' 297 | ].join(' ')) 298 | body.push(':rotating_light::rotating_light::rotating_light:') 299 | } 300 | return body.join('\n\n').trim() 301 | - name: Update Release PR Comment 302 | if: steps.comment-text.outputs.result 303 | uses: peter-evans/create-or-update-comment@v3 304 | with: 305 | comment-id: ${{ steps.found-comment.outputs.comment-id }} 306 | body: ${{ steps.comment-text.outputs.result }} 307 | edit-mode: 'replace' 308 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | # ignore everything in the root 4 | /* 5 | # transient test directories 6 | tap-testdir*/ 7 | 8 | # keep these 9 | !**/.gitignore 10 | !/.commitlintrc.js 11 | !/.eslintrc.js 12 | !/.eslintrc.local.* 13 | !/.github/ 14 | !/.gitignore 15 | !/.npmrc 16 | !/.release-please-manifest.json 17 | !/bin/ 18 | !/CHANGELOG* 19 | !/CODE_OF_CONDUCT.md 20 | !/CONTRIBUTING.md 21 | !/docs/ 22 | !/lib/ 23 | !/LICENSE* 24 | !/map.js 25 | !/package.json 26 | !/README* 27 | !/release-please-config.json 28 | !/scripts/ 29 | !/SECURITY.md 30 | !/tap-snapshots/ 31 | !/test/ 32 | !/tsconfig.json 33 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ; This file is automatically added by @npmcli/template-oss. Do not edit. 2 | 3 | package-lock=false 4 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "5.0.2" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [5.0.2](https://github.com/npm/gauge/compare/v5.0.1...v5.0.2) (2024-05-04) 4 | 5 | ### Bug Fixes 6 | 7 | * [`d772b3b`](https://github.com/npm/gauge/commit/d772b3b034b60164a4eed59b6bd6275fa14bfdc1) [#239](https://github.com/npm/gauge/pull/239) linting: no-unused-vars (@lukekarrys) 8 | 9 | ### Chores 10 | 11 | * [`c8aec3d`](https://github.com/npm/gauge/commit/c8aec3dc8c8ac12cf42c88eafface85d60243ad4) [#239](https://github.com/npm/gauge/pull/239) bump @npmcli/template-oss to 4.22.0 (@lukekarrys) 12 | * [`54421e8`](https://github.com/npm/gauge/commit/54421e8076ab455847f9fe71d8444ba7d8e535a7) [#239](https://github.com/npm/gauge/pull/239) postinstall for dependabot template-oss PR (@lukekarrys) 13 | * [`ba82947`](https://github.com/npm/gauge/commit/ba829473c0df3bebb6f4d8ec98b951274119b30b) [#238](https://github.com/npm/gauge/pull/238) bump @npmcli/template-oss from 4.21.3 to 4.21.4 (@dependabot[bot]) 14 | 15 | ## [5.0.1](https://github.com/npm/gauge/compare/v5.0.0...v5.0.1) (2023-04-26) 16 | 17 | ### Dependencies 18 | 19 | * [`88057a0`](https://github.com/npm/gauge/commit/88057a0dcba98a0e03e92b6af15b8700ee48ac84) [#199](https://github.com/npm/gauge/pull/199) bump signal-exit from 3.0.7 to 4.0.1 (#199) 20 | 21 | ## [5.0.0](https://github.com/npm/gauge/compare/v4.0.4...v5.0.0) (2022-10-10) 22 | 23 | ### ⚠️ BREAKING CHANGES 24 | 25 | * `gauge` is now compatible with the following semver range for node: `^14.17.0 || ^16.13.0 || >=18.0.0` 26 | 27 | ### Features 28 | 29 | * [`6f3535a`](https://github.com/npm/gauge/commit/6f3535afd4a37f0ad025b0cb1545b189d826681b) [#181](https://github.com/npm/gauge/pull/181) postinstall for dependabot template-oss PR (@lukekarrys) 30 | 31 | ### [4.0.4](https://github.com/npm/gauge/compare/v4.0.3...v4.0.4) (2022-03-28) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * fix always true condition ([#160](https://github.com/npm/gauge/issues/160)) ([bebaf0b](https://github.com/npm/gauge/commit/bebaf0b0655f0bdc58a6548b04230cd420245e5e)) 37 | 38 | ### v4.0.0 39 | 40 | * BREAKING CHANGE: Drops support for Node v10 and non-LTS versions of v12 and v14 41 | * feat: install template-oss 42 | * fix: repository metadata 43 | * fix: Typo in package.json 44 | * fix: Remove remaining uses of object-assign 45 | * fix: remove object-assign 46 | 47 | ### [4.0.3](https://www.github.com/npm/gauge/compare/v4.0.2...v4.0.3) (2022-03-10) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * move demo.js to the docs folder ([#155](https://www.github.com/npm/gauge/issues/155)) ([1faf8cf](https://www.github.com/npm/gauge/commit/1faf8cf25c0bffb345f6657e20743d83c54d6695)) 53 | 54 | 55 | ### Dependencies 56 | 57 | * remove the unused direct ansi-regex dependency ([#156](https://www.github.com/npm/gauge/issues/156)) ([65be798](https://www.github.com/npm/gauge/commit/65be79895801433e02aef58cafb6f31a87287e59)) 58 | 59 | ### [4.0.2](https://www.github.com/npm/gauge/compare/v4.0.1...v4.0.2) (2022-02-16) 60 | 61 | 62 | ### Dependencies 63 | 64 | * update color-support requirement from ^1.1.2 to ^1.1.3 ([5921a0f](https://www.github.com/npm/gauge/commit/5921a0f89e6c4c10ea047a219230809fd4118409)) 65 | * update console-control-strings requirement from ^1.0.0 to ^1.1.0 ([a5ac787](https://www.github.com/npm/gauge/commit/a5ac787a3771e8882f9837fab08ca2985cad609f)) 66 | * update signal-exit requirement from ^3.0.0 to ^3.0.7 ([3e0d399](https://www.github.com/npm/gauge/commit/3e0d39917b10e0f94efd4a4c74a46fa8e21e768a)) 67 | * update wide-align requirement from ^1.1.2 to ^1.1.5 ([ddc9048](https://www.github.com/npm/gauge/commit/ddc90480a6c1caa8c176e0b65a9d8207be846f94)) 68 | 69 | ### [4.0.1](https://www.github.com/npm/gauge/compare/v4.0.0...v4.0.1) (2022-02-15) 70 | 71 | 72 | ### Bug Fixes 73 | 74 | * use more commonly available character for pre/post progressbar ([#141](https://www.github.com/npm/gauge/issues/141)) ([13d3046](https://www.github.com/npm/gauge/commit/13d30466b56015cb75df366371cf85234a8a517f)) 75 | 76 | ### v3.0.1 77 | 78 | * deps: object-assign@4.1.1 79 | 80 | ### v3.0.0 81 | * BREAKING CHANGE: Drops support for Node v4, v6, v7 and v8 82 | 83 | ### v2.7.4 84 | 85 | * Reset colors prior to ending a line, to eliminate flicker when a line 86 | is trucated between start and end color sequences. 87 | 88 | ### v2.7.3 89 | 90 | * Only create our onExit handler when we're enabled and remove it when we're 91 | disabled. This stops us from creating multiple onExit handlers when 92 | multiple gauge objects are being used. 93 | * Fix bug where if a theme name were given instead of a theme object, it 94 | would crash. 95 | * Remove supports-color because it's not actually used. Uhm. Yes, I just 96 | updated it. >.> 97 | 98 | ### v2.7.2 99 | 100 | * Use supports-color instead of has-color (as the module has been renamed) 101 | 102 | ### v2.7.1 103 | 104 | * Bug fix: Calls to show/pulse while the progress bar is disabled should still 105 | update our internal representation of what would be shown should it be enabled. 106 | 107 | ### v2.7.0 108 | 109 | * New feature: Add new `isEnabled` method to allow introspection of the gauge's 110 | "enabledness" as controlled by `.enable()` and `.disable()`. 111 | 112 | ### v2.6.0 113 | 114 | * Bug fix: Don't run the code associated with `enable`/`disable` if the gauge 115 | is already enabled or disabled respectively. This prevents leaking event 116 | listeners, amongst other weirdness. 117 | * New feature: Template items can have default values that will be used if no 118 | value was otherwise passed in. 119 | 120 | ### v2.5.3 121 | 122 | * Default to `enabled` only if we have a tty. Users can always override 123 | this by passing in the `enabled` option explicitly or by calling calling 124 | `gauge.enable()`. 125 | 126 | ### v2.5.2 127 | 128 | * Externalized `./console-strings.js` into `console-control-strings`. 129 | 130 | ### v2.5.1 131 | 132 | * Update to `signal-exit@3.0.0`, which fixes a compatibility bug with the 133 | node profiler. 134 | * [#39](https://github.com/iarna/gauge/pull/39) Fix tests on 0.10 and add 135 | a missing devDependency. ([@helloyou2012](https://github.com/helloyou2012)) 136 | 137 | ### v2.5.0 138 | 139 | * Add way to programmatically fetch a list of theme names in a themeset 140 | (`Themeset.getThemeNames`). 141 | 142 | ### v2.4.0 143 | 144 | * Add support for setting themesets on existing gauge objects. 145 | * Add post-IO callback to `gauge.hide()` as it is somtetimes necessary when 146 | your terminal is interleaving output from multiple filehandles (ie, stdout 147 | & stderr). 148 | 149 | ### v2.3.1 150 | 151 | * Fix a refactor bug in setTheme where it wasn't accepting the various types 152 | of args it should. 153 | 154 | ### v2.3.0 155 | 156 | #### FEATURES 157 | 158 | * Add setTemplate & setTheme back in. 159 | * Add support for named themes, you can now ask for things like 'colorASCII' 160 | and 'brailleSpinner'. Of course, you can still pass in theme objects. 161 | Additionally you can now pass in an object with `hasUnicode`, `hasColor` and 162 | `platform` keys in order to override our guesses as to those values when 163 | selecting a default theme from the themeset. 164 | * Make the output stream optional (it defaults to `process.stderr` now). 165 | * Add `setWriteTo(stream[, tty])` to change the output stream and, 166 | optionally, tty. 167 | 168 | #### BUG FIXES & REFACTORING 169 | 170 | * Abort the display phase early if we're supposed to be hidden and we are. 171 | * Stop printing a bunch of spaces at the end of lines, since we're already 172 | using an erase-to-end-of-line code anyway. 173 | * The unicode themes were missing the subsection separator. 174 | 175 | ### v2.2.1 176 | 177 | * Fix image in readme 178 | 179 | ### v2.2.0 180 | 181 | * All new themes API– reference themes by name and pass in custom themes and 182 | themesets (themesets get platform support autodetection done on them to 183 | select the best theme). Theme mixins let you add features to all existing 184 | themes. 185 | * Much, much improved test coverage. 186 | 187 | ### v2.1.0 188 | 189 | * Got rid of ░ in the default platform, noUnicode, hasColor theme. Thanks 190 | to @yongtw123 for pointing out this had snuck in. 191 | * Fiddled with the demo output to make it easier to see the spinner spin. Also 192 | added prints before each platforms test output. 193 | * I forgot to include `signal-exit` in our deps. <.< Thank you @KenanY for 194 | finding this. Then I was lazy and made a new commit instead of using his 195 | PR. Again, thank you for your patience @KenenY. 196 | * Drastically speed up travis testing. 197 | * Add a small javascript demo (demo.js) for showing off the various themes 198 | (and testing them on diff platforms). 199 | * Change: The subsection separator from ⁄ and / (different chars) to >. 200 | * Fix crasher: A show or pulse without a label would cause the template renderer 201 | to complain about a missing value. 202 | * New feature: Add the ability to disable the clean-up-on-exit behavior. 203 | Not something I expect to be widely desirable, but important if you have 204 | multiple distinct gauge instances in your app. 205 | * Use our own color support detection. 206 | The `has-color` module proved too magic for my needs, making assumptions 207 | as to which stream we write to and reading command line arguments. 208 | 209 | ### v2.0.0 210 | 211 | This is a major rewrite of the internals. Externally there are fewer 212 | changes: 213 | 214 | * On node>0.8 gauge object now prints updates at a fixed rate. This means 215 | that when you call `show` it may wate up to `updateInterval` ms before it 216 | actually prints an update. You override this behavior with the 217 | `fixedFramerate` option. 218 | * The gauge object now keeps the cursor hidden as long as it's enabled and 219 | shown. 220 | * The constructor's arguments have changed, now it takes a mandatory output 221 | stream and an optional options object. The stream no longer needs to be 222 | an `ansi`ified stream, although it can be if you want (but we won't make 223 | use of its special features). 224 | * Previously the gauge was disabled by default if `process.stdout` wasn't a 225 | tty. Now it always defaults to enabled. If you want the previous 226 | behavior set the `enabled` option to `process.stdout.isTTY`. 227 | * The constructor's options have changed– see the docs for details. 228 | * Themes are entirely different. If you were using a custom theme, or 229 | referring to one directly (eg via `Gauge.unicode` or `Gauge.ascii`) then 230 | you'll need to change your code. You can get the equivalent of the latter 231 | with: 232 | ``` 233 | var themes = require('gauge/themes') 234 | var unicodeTheme = themes(true, true) // returns the color unicode theme for your platform 235 | ``` 236 | The default themes no longer use any ambiguous width characters, so even 237 | if you choose to display those as wide your progress bar should still 238 | display correctly. 239 | * Templates are entirely different and if you were using a custom one, you 240 | should consult the documentation to learn how to recreate it. If you were 241 | using the default, be aware that it has changed and the result looks quite 242 | a bit different. 243 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | All interactions in this repo are covered by the [npm Code of 4 | Conduct](https://docs.npmjs.com/policies/conduct) 5 | 6 | The npm cli team may, at its own discretion, moderate, remove, or edit 7 | any interactions such as pull requests, issues, and comments. 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | # Contributing 4 | 5 | ## Code of Conduct 6 | 7 | All interactions in the **npm** organization on GitHub are considered to be covered by our standard [Code of Conduct](https://docs.npmjs.com/policies/conduct). 8 | 9 | ## Reporting Bugs 10 | 11 | Before submitting a new bug report please search for an existing or similar report. 12 | 13 | Use one of our existing issue templates if you believe you've come across a unique problem. 14 | 15 | Duplicate issues, or issues that don't use one of our templates may get closed without a response. 16 | 17 | ## Pull Request Conventions 18 | 19 | ### Commits 20 | 21 | We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). 22 | 23 | When opening a pull request please be sure that either the pull request title, or each commit in the pull request, has one of the following prefixes: 24 | 25 | - `feat`: For when introducing a new feature. The result will be a new semver minor version of the package when it is next published. 26 | - `fix`: For bug fixes. The result will be a new semver patch version of the package when it is next published. 27 | - `docs`: For documentation updates. The result will be a new semver patch version of the package when it is next published. 28 | - `chore`: For changes that do not affect the published module. Often these are changes to tests. The result will be *no* change to the version of the package when it is next published (as the commit does not affect the published version). 29 | 30 | ### Test Coverage 31 | 32 | Pull requests made against this repo will run `npm test` automatically. Please make sure tests pass locally before submitting a PR. 33 | 34 | Every new feature or bug fix should come with a corresponding test or tests that validate the solutions. Testing also reports on code coverage and will fail if code coverage drops. 35 | 36 | ### Linting 37 | 38 | Linting is also done automatically once tests pass. `npm run lintfix` will fix most linting errors automatically. 39 | 40 | Please make sure linting passes before submitting a PR. 41 | 42 | ## What _not_ to contribute? 43 | 44 | ### Dependencies 45 | 46 | It should be noted that our team does not accept third-party dependency updates/PRs. If you submit a PR trying to update our dependencies we will close it with or without a reference to these contribution guidelines. 47 | 48 | ### Tools/Automation 49 | 50 | Our core team is responsible for the maintenance of the tooling/automation in this project and we ask contributors to not make changes to these when contributing (e.g. `.github/*`, `.eslintrc.json`, `.licensee.json`). Most of those files also have a header at the top to remind folks they are automatically generated. Pull requests that alter these will not be accepted. 51 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | ISC License 4 | 5 | Copyright npm, Inc. 6 | 7 | Permission to use, copy, modify, and/or distribute this 8 | software for any purpose with or without fee is hereby 9 | granted, provided that the above copyright notice and this 10 | permission notice appear in all copies. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS" AND NPM DISCLAIMS ALL 13 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO 15 | EVENT SHALL NPM BE LIABLE FOR ANY SPECIAL, DIRECT, 16 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 17 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 18 | WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 19 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE 20 | USE OR PERFORMANCE OF THIS SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gauge 2 | ===== 3 | 4 | A nearly stateless terminal based horizontal gauge / progress bar. 5 | 6 | ```javascript 7 | var Gauge = require("gauge") 8 | 9 | var gauge = new Gauge() 10 | 11 | gauge.show("working…", 0) 12 | setTimeout(() => { gauge.pulse(); gauge.show("working…", 0.25) }, 500) 13 | setTimeout(() => { gauge.pulse(); gauge.show("working…", 0.50) }, 1000) 14 | setTimeout(() => { gauge.pulse(); gauge.show("working…", 0.75) }, 1500) 15 | setTimeout(() => { gauge.pulse(); gauge.show("working…", 0.99) }, 2000) 16 | setTimeout(() => gauge.hide(), 2300) 17 | ``` 18 | 19 | See also the [demos](docs/demo.js): 20 | 21 | ![](./docs/gauge-demo.gif) 22 | 23 | 24 | ### CHANGES FROM 1.x 25 | 26 | Gauge 2.x is breaking release, please see the [changelog] for details on 27 | what's changed if you were previously a user of this module. 28 | 29 | [changelog]: CHANGELOG.md 30 | 31 | ### THE GAUGE CLASS 32 | 33 | This is the typical interface to the module– it provides a pretty 34 | fire-and-forget interface to displaying your status information. 35 | 36 | ``` 37 | var Gauge = require("gauge") 38 | 39 | var gauge = new Gauge([stream], [options]) 40 | ``` 41 | 42 | * **stream** – *(optional, default STDERR)* A stream that progress bar 43 | updates are to be written to. Gauge honors backpressure and will pause 44 | most writing if it is indicated. 45 | * **options** – *(optional)* An option object. 46 | 47 | Constructs a new gauge. Gauges are drawn on a single line, and are not drawn 48 | if **stream** isn't a tty and a tty isn't explicitly provided. 49 | 50 | If **stream** is a terminal or if you pass in **tty** to **options** then we 51 | will detect terminal resizes and redraw to fit. We do this by watching for 52 | `resize` events on the tty. (To work around a bug in versions of Node prior 53 | to 2.5.0, we watch for them on stdout if the tty is stderr.) Resizes to 54 | larger window sizes will be clean, but shrinking the window will always 55 | result in some cruft. 56 | 57 | **IMPORTANT:** If you previously were passing in a non-tty stream but you still 58 | want output (for example, a stream wrapped by the `ansi` module) then you 59 | need to pass in the **tty** option below, as `gauge` needs access to 60 | the underlying tty in order to do things like terminal resizes and terminal 61 | width detection. 62 | 63 | The **options** object can have the following properties, all of which are 64 | optional: 65 | 66 | * **updateInterval**: How often gauge updates should be drawn, in milliseconds. 67 | * **fixedFramerate**: Defaults to false on node 0.8, true on everything 68 | else. When this is true a timer is created to trigger once every 69 | `updateInterval` ms, when false, updates are printed as soon as they come 70 | in but updates more often than `updateInterval` are ignored. The reason 71 | 0.8 doesn't have this set to true is that it can't `unref` its timer and 72 | so it would stop your program from exiting– if you want to use this 73 | feature with 0.8 just make sure you call `gauge.disable()` before you 74 | expect your program to exit. 75 | * **themes**: A themeset to use when selecting the theme to use. Defaults 76 | to `gauge/themes`, see the [themes] documentation for details. 77 | * **theme**: Select a theme for use, it can be a: 78 | * Theme object, in which case the **themes** is not used. 79 | * The name of a theme, which will be looked up in the current *themes* 80 | object. 81 | * A configuration object with any of `hasUnicode`, `hasColor` or 82 | `platform` keys, which if will be used to override our guesses when making 83 | a default theme selection. 84 | 85 | If no theme is selected then a default is picked using a combination of our 86 | best guesses at your OS, color support and unicode support. 87 | * **template**: Describes what you want your gauge to look like. The 88 | default is what npm uses. Detailed [documentation] is later in this 89 | document. 90 | * **hideCursor**: Defaults to true. If true, then the cursor will be hidden 91 | while the gauge is displayed. 92 | * **tty**: The tty that you're ultimately writing to. Defaults to the same 93 | as **stream**. This is used for detecting the width of the terminal and 94 | resizes. The width used is `tty.columns - 1`. If no tty is available then 95 | a width of `79` is assumed. 96 | * **enabled**: Defaults to true if `tty` is a TTY, false otherwise. If true 97 | the gauge starts enabled. If disabled then all update commands are 98 | ignored and no gauge will be printed until you call `.enable()`. 99 | * **Plumbing**: The class to use to actually generate the gauge for 100 | printing. This defaults to `require('gauge/plumbing')` and ordinarily you 101 | shouldn't need to override this. 102 | * **cleanupOnExit**: Defaults to true. Ordinarily we register an exit 103 | handler to make sure your cursor is turned back on and the progress bar 104 | erased when your process exits, even if you Ctrl-C out or otherwise exit 105 | unexpectedly. You can disable this and it won't register the exit handler. 106 | 107 | [has-unicode]: https://www.npmjs.com/package/has-unicode 108 | [themes]: #themes 109 | [documentation]: #templates 110 | 111 | #### `gauge.show(section | status, [completed])` 112 | 113 | The first argument is either the section, the name of the current thing 114 | contributing to progress, or an object with keys like **section**, 115 | **subsection** & **completed** (or any others you have types for in a custom 116 | template). If you don't want to update or set any of these you can pass 117 | `null` and it will be ignored. 118 | 119 | The second argument is the percent completed as a value between 0 and 1. 120 | Without it, completion is just not updated. You'll also note that completion 121 | can be passed in as part of a status object as the first argument. If both 122 | it and the completed argument are passed in, the completed argument wins. 123 | 124 | #### `gauge.hide([cb])` 125 | 126 | Removes the gauge from the terminal. Optionally, callback `cb` after IO has 127 | had an opportunity to happen (currently this just means after `setImmediate` 128 | has called back.) 129 | 130 | It turns out this is important when you're pausing the progress bar on one 131 | filehandle and printing to another– otherwise (with a big enough print) node 132 | can end up printing the "end progress bar" bits to the progress bar filehandle 133 | while other stuff is printing to another filehandle. These getting interleaved 134 | can cause corruption in some terminals. 135 | 136 | #### `gauge.pulse([subsection])` 137 | 138 | * **subsection** – *(optional)* The specific thing that triggered this pulse 139 | 140 | Spins the spinner in the gauge to show output. If **subsection** is 141 | included then it will be combined with the last name passed to `gauge.show`. 142 | 143 | #### `gauge.disable()` 144 | 145 | Hides the gauge and ignores further calls to `show` or `pulse`. 146 | 147 | #### `gauge.enable()` 148 | 149 | Shows the gauge and resumes updating when `show` or `pulse` is called. 150 | 151 | #### `gauge.isEnabled()` 152 | 153 | Returns true if the gauge is enabled. 154 | 155 | #### `gauge.setThemeset(themes)` 156 | 157 | Change the themeset to select a theme from. The same as the `themes` option 158 | used in the constructor. The theme will be reselected from this themeset. 159 | 160 | #### `gauge.setTheme(theme)` 161 | 162 | Change the active theme, will be displayed with the next show or pulse. This can be: 163 | 164 | * Theme object, in which case the **themes** is not used. 165 | * The name of a theme, which will be looked up in the current *themes* 166 | object. 167 | * A configuration object with any of `hasUnicode`, `hasColor` or 168 | `platform` keys, which if will be used to override our guesses when making 169 | a default theme selection. 170 | 171 | If no theme is selected then a default is picked using a combination of our 172 | best guesses at your OS, color support and unicode support. 173 | 174 | #### `gauge.setTemplate(template)` 175 | 176 | Change the active template, will be displayed with the next show or pulse 177 | 178 | ### Tracking Completion 179 | 180 | If you have more than one thing going on that you want to track completion 181 | of, you may find the related [are-we-there-yet] helpful. It's `change` 182 | event can be wired up to the `show` method to get a more traditional 183 | progress bar interface. 184 | 185 | [are-we-there-yet]: https://www.npmjs.com/package/are-we-there-yet 186 | 187 | ### THEMES 188 | 189 | ``` 190 | var themes = require('gauge/themes') 191 | 192 | // fetch the default color unicode theme for this platform 193 | var ourTheme = themes({hasUnicode: true, hasColor: true}) 194 | 195 | // fetch the default non-color unicode theme for osx 196 | var ourTheme = themes({hasUnicode: true, hasColor: false, platform: 'darwin'}) 197 | 198 | // create a new theme based on the color ascii theme for this platform 199 | // that brackets the progress bar with arrows 200 | var ourTheme = themes.newTheme(themes({hasUnicode: false, hasColor: true}), { 201 | preProgressbar: '→', 202 | postProgressbar: '←' 203 | }) 204 | ``` 205 | 206 | The object returned by `gauge/themes` is an instance of the `ThemeSet` class. 207 | 208 | ``` 209 | var ThemeSet = require('gauge/theme-set') 210 | var themes = new ThemeSet() 211 | // or 212 | var themes = require('gauge/themes') 213 | var mythemes = themes.newThemeSet() // creates a new themeset based on the default themes 214 | ``` 215 | 216 | #### themes(opts) 217 | #### themes.getDefault(opts) 218 | 219 | Theme objects are a function that fetches the default theme based on 220 | platform, unicode and color support. 221 | 222 | Options is an object with the following properties: 223 | 224 | * **hasUnicode** - If true, fetch a unicode theme, if no unicode theme is 225 | available then a non-unicode theme will be used. 226 | * **hasColor** - If true, fetch a color theme, if no color theme is 227 | available a non-color theme will be used. 228 | * **platform** (optional) - Defaults to `process.platform`. If no 229 | platform match is available then `fallback` is used instead. 230 | 231 | If no compatible theme can be found then an error will be thrown with a 232 | `code` of `EMISSINGTHEME`. 233 | 234 | #### themes.addTheme(themeName, themeObj) 235 | #### themes.addTheme(themeName, [parentTheme], newTheme) 236 | 237 | Adds a named theme to the themeset. You can pass in either a theme object, 238 | as returned by `themes.newTheme` or the arguments you'd pass to 239 | `themes.newTheme`. 240 | 241 | #### themes.getThemeNames() 242 | 243 | Return a list of all of the names of the themes in this themeset. Suitable 244 | for use in `themes.getTheme(…)`. 245 | 246 | #### themes.getTheme(name) 247 | 248 | Returns the theme object from this theme set named `name`. 249 | 250 | If `name` does not exist in this themeset an error will be thrown with 251 | a `code` of `EMISSINGTHEME`. 252 | 253 | #### themes.setDefault([opts], themeName) 254 | 255 | `opts` is an object with the following properties. 256 | 257 | * **platform** - Defaults to `'fallback'`. If your theme is platform 258 | specific, specify that here with the platform from `process.platform`, eg, 259 | `win32`, `darwin`, etc. 260 | * **hasUnicode** - Defaults to `false`. If your theme uses unicode you 261 | should set this to true. 262 | * **hasColor** - Defaults to `false`. If your theme uses color you should 263 | set this to true. 264 | 265 | `themeName` is the name of the theme (as given to `addTheme`) to use for 266 | this set of `opts`. 267 | 268 | #### themes.newTheme([parentTheme,] newTheme) 269 | 270 | Create a new theme object based on `parentTheme`. If no `parentTheme` is 271 | provided then a minimal parentTheme that defines functions for rendering the 272 | activity indicator (spinner) and progress bar will be defined. (This 273 | fallback parent is defined in `gauge/base-theme`.) 274 | 275 | newTheme should be a bare object– we'll start by discussing the properties 276 | defined by the default themes: 277 | 278 | * **preProgressbar** - displayed prior to the progress bar, if the progress 279 | bar is displayed. 280 | * **postProgressbar** - displayed after the progress bar, if the progress bar 281 | is displayed. 282 | * **progressBarTheme** - The subtheme passed through to the progress bar 283 | renderer, it's an object with `complete` and `remaining` properties 284 | that are the strings you want repeated for those sections of the progress 285 | bar. 286 | * **activityIndicatorTheme** - The theme for the activity indicator (spinner), 287 | this can either be a string, in which each character is a different step, or 288 | an array of strings. 289 | * **preSubsection** - Displayed as a separator between the `section` and 290 | `subsection` when the latter is printed. 291 | 292 | More generally, themes can have any value that would be a valid value when rendering 293 | templates. The properties in the theme are used when their name matches a type in 294 | the template. Their values can be: 295 | 296 | * **strings & numbers** - They'll be included as is 297 | * **function (values, theme, width)** - Should return what you want in your output. 298 | *values* is an object with values provided via `gauge.show`, 299 | *theme* is the theme specific to this item (see below) or this theme object, 300 | and *width* is the number of characters wide your result should be. 301 | 302 | There are a couple of special prefixes: 303 | 304 | * **pre** - Is shown prior to the property, if its displayed. 305 | * **post** - Is shown after the property, if its displayed. 306 | 307 | And one special suffix: 308 | 309 | * **Theme** - Its value is passed to a function-type item as the theme. 310 | 311 | #### themes.addToAllThemes(theme) 312 | 313 | This *mixes-in* `theme` into all themes currently defined. It also adds it 314 | to the default parent theme for this themeset, so future themes added to 315 | this themeset will get the values from `theme` by default. 316 | 317 | #### themes.newThemeSet() 318 | 319 | Copy the current themeset into a new one. This allows you to easily inherit 320 | one themeset from another. 321 | 322 | ### TEMPLATES 323 | 324 | A template is an array of objects and strings that, after being evaluated, 325 | will be turned into the gauge line. The default template is: 326 | 327 | ```javascript 328 | [ 329 | {type: 'progressbar', length: 20}, 330 | {type: 'activityIndicator', kerning: 1, length: 1}, 331 | {type: 'section', kerning: 1, default: ''}, 332 | {type: 'subsection', kerning: 1, default: ''} 333 | ] 334 | ``` 335 | 336 | The various template elements can either be **plain strings**, in which case they will 337 | be be included verbatum in the output, or objects with the following properties: 338 | 339 | * *type* can be any of the following plus any keys you pass into `gauge.show` plus 340 | any keys you have on a custom theme. 341 | * `section` – What big thing you're working on now. 342 | * `subsection` – What component of that thing is currently working. 343 | * `activityIndicator` – Shows a spinner using the `activityIndicatorTheme` 344 | from your active theme. 345 | * `progressbar` – A progress bar representing your current `completed` 346 | using the `progressbarTheme` from your active theme. 347 | * *kerning* – Number of spaces that must be between this item and other 348 | items, if this item is displayed at all. 349 | * *maxLength* – The maximum length for this element. If its value is longer it 350 | will be truncated. 351 | * *minLength* – The minimum length for this element. If its value is shorter it 352 | will be padded according to the *align* value. 353 | * *align* – (Default: left) Possible values "left", "right" and "center". Works 354 | as you'd expect from word processors. 355 | * *length* – Provides a single value for both *minLength* and *maxLength*. If both 356 | *length* and *minLength or *maxLength* are specified then the latter take precedence. 357 | * *value* – A literal value to use for this template item. 358 | * *default* – A default value to use for this template item if a value 359 | wasn't otherwise passed in. 360 | 361 | ### PLUMBING 362 | 363 | This is the super simple, assume nothing, do no magic internals used by gauge to 364 | implement its ordinary interface. 365 | 366 | ``` 367 | var Plumbing = require('gauge/plumbing') 368 | var gauge = new Plumbing(theme, template, width) 369 | ``` 370 | 371 | * **theme**: The theme to use. 372 | * **template**: The template to use. 373 | * **width**: How wide your gauge should be 374 | 375 | #### `gauge.setTheme(theme)` 376 | 377 | Change the active theme. 378 | 379 | #### `gauge.setTemplate(template)` 380 | 381 | Change the active template. 382 | 383 | #### `gauge.setWidth(width)` 384 | 385 | Change the width to render at. 386 | 387 | #### `gauge.hide()` 388 | 389 | Return the string necessary to hide the progress bar 390 | 391 | #### `gauge.hideCursor()` 392 | 393 | Return a string to hide the cursor. 394 | 395 | #### `gauge.showCursor()` 396 | 397 | Return a string to show the cursor. 398 | 399 | #### `gauge.show(status)` 400 | 401 | Using `status` for values, render the provided template with the theme and return 402 | a string that is suitable for printing to update the gauge. 403 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | <!-- This file is automatically added by @npmcli/template-oss. Do not edit. --> 2 | 3 | GitHub takes the security of our software products and services seriously, including the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub). 4 | 5 | If you believe you have found a security vulnerability in this GitHub-owned open source repository, you can report it to us in one of two ways. 6 | 7 | If the vulnerability you have found is *not* [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) or if you do not wish to be considered for a bounty reward, please report the issue to us directly through [opensource-security@github.com](mailto:opensource-security@github.com). 8 | 9 | If the vulnerability you have found is [in scope for the GitHub Bug Bounty Program](https://bounty.github.com/#scope) and you would like for your finding to be considered for a bounty reward, please submit the vulnerability to us through [HackerOne](https://hackerone.com/github) in order to be eligible to receive a bounty award. 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** 12 | 13 | Thanks for helping make GitHub safe for everyone. 14 | -------------------------------------------------------------------------------- /docs/demo.js: -------------------------------------------------------------------------------- 1 | var Gauge = require('..') 2 | var gaugeDefault = require('../lib/themes.js') 3 | var onExit = require('signal-exit').onExit 4 | 5 | var activeGauge 6 | 7 | onExit(function () { 8 | activeGauge.disable() 9 | }) 10 | 11 | var themes = gaugeDefault.getThemeNames() 12 | 13 | nextBar() 14 | function nextBar () { 15 | var themeName = themes.shift() 16 | 17 | console.log('Demoing output for ' + themeName) 18 | 19 | var gt = new Gauge(process.stderr, { 20 | updateInterval: 50, 21 | theme: themeName, 22 | cleanupOnExit: false, 23 | }) 24 | activeGauge = gt 25 | 26 | var progress = 0 27 | 28 | var cnt = 0 29 | var pulse = setInterval(function () { 30 | gt.pulse('this is a thing that happened ' + (++cnt)) 31 | }, 110) 32 | var prog = setInterval(function () { 33 | progress += 0.04 34 | gt.show(themeName + ':' + Math.round(progress * 1000), progress) 35 | if (progress >= 1) { 36 | clearInterval(prog) 37 | clearInterval(pulse) 38 | gt.disable() 39 | if (themes.length) { 40 | nextBar() 41 | } 42 | } 43 | }, 100) 44 | gt.show() 45 | } 46 | -------------------------------------------------------------------------------- /docs/gauge-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/npm/gauge/f8092518a47ac6a96027ae3ad97d0251ffe7643b/docs/gauge-demo.gif -------------------------------------------------------------------------------- /lib/base-theme.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var spin = require('./spin.js') 3 | var progressBar = require('./progress-bar.js') 4 | 5 | module.exports = { 6 | activityIndicator: function (values, theme) { 7 | if (values.spun == null) { 8 | return 9 | } 10 | return spin(theme, values.spun) 11 | }, 12 | progressbar: function (values, theme, width) { 13 | if (values.completed == null) { 14 | return 15 | } 16 | return progressBar(theme, width, values.completed) 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /lib/error.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var util = require('util') 3 | 4 | var User = exports.User = function User (msg) { 5 | var err = new Error(msg) 6 | Error.captureStackTrace(err, User) 7 | err.code = 'EGAUGE' 8 | return err 9 | } 10 | 11 | exports.MissingTemplateValue = function MissingTemplateValue (item, values) { 12 | var err = new User(util.format('Missing template value "%s"', item.type)) 13 | Error.captureStackTrace(err, MissingTemplateValue) 14 | err.template = item 15 | err.values = values 16 | return err 17 | } 18 | 19 | exports.Internal = function Internal (msg) { 20 | var err = new Error(msg) 21 | Error.captureStackTrace(err, Internal) 22 | err.code = 'EGAUGEINTERNAL' 23 | return err 24 | } 25 | -------------------------------------------------------------------------------- /lib/has-color.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var colorSupport = require('color-support') 3 | 4 | module.exports = colorSupport().hasBasic 5 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var Plumbing = require('./plumbing.js') 3 | var hasUnicode = require('has-unicode') 4 | var hasColor = require('./has-color.js') 5 | var onExit = require('signal-exit').onExit 6 | var defaultThemes = require('./themes') 7 | var setInterval = require('./set-interval.js') 8 | var process = require('./process.js') 9 | var setImmediate = require('./set-immediate') 10 | 11 | module.exports = Gauge 12 | 13 | function callWith (obj, method) { 14 | return function () { 15 | return method.call(obj) 16 | } 17 | } 18 | 19 | function Gauge (arg1, arg2) { 20 | var options, writeTo 21 | if (arg1 && arg1.write) { 22 | writeTo = arg1 23 | options = arg2 || {} 24 | } else if (arg2 && arg2.write) { 25 | writeTo = arg2 26 | options = arg1 || {} 27 | } else { 28 | writeTo = process.stderr 29 | options = arg1 || arg2 || {} 30 | } 31 | 32 | this._status = { 33 | spun: 0, 34 | section: '', 35 | subsection: '', 36 | } 37 | this._paused = false // are we paused for back pressure? 38 | this._disabled = true // are all progress bar updates disabled? 39 | this._showing = false // do we WANT the progress bar on screen 40 | this._onScreen = false // IS the progress bar on screen 41 | this._needsRedraw = false // should we print something at next tick? 42 | this._hideCursor = options.hideCursor == null ? true : options.hideCursor 43 | this._fixedFramerate = options.fixedFramerate == null 44 | ? !(/^v0\.8\./.test(process.version)) 45 | : options.fixedFramerate 46 | this._lastUpdateAt = null 47 | this._updateInterval = options.updateInterval == null ? 50 : options.updateInterval 48 | 49 | this._themes = options.themes || defaultThemes 50 | this._theme = options.theme 51 | var theme = this._computeTheme(options.theme) 52 | var template = options.template || [ 53 | { type: 'progressbar', length: 20 }, 54 | { type: 'activityIndicator', kerning: 1, length: 1 }, 55 | { type: 'section', kerning: 1, default: '' }, 56 | { type: 'subsection', kerning: 1, default: '' }, 57 | ] 58 | this.setWriteTo(writeTo, options.tty) 59 | var PlumbingClass = options.Plumbing || Plumbing 60 | this._gauge = new PlumbingClass(theme, template, this.getWidth()) 61 | 62 | this._$$doRedraw = callWith(this, this._doRedraw) 63 | this._$$handleSizeChange = callWith(this, this._handleSizeChange) 64 | 65 | this._cleanupOnExit = options.cleanupOnExit == null || options.cleanupOnExit 66 | this._removeOnExit = null 67 | 68 | if (options.enabled || (options.enabled == null && this._tty && this._tty.isTTY)) { 69 | this.enable() 70 | } else { 71 | this.disable() 72 | } 73 | } 74 | Gauge.prototype = {} 75 | 76 | Gauge.prototype.isEnabled = function () { 77 | return !this._disabled 78 | } 79 | 80 | Gauge.prototype.setTemplate = function (template) { 81 | this._gauge.setTemplate(template) 82 | if (this._showing) { 83 | this._requestRedraw() 84 | } 85 | } 86 | 87 | Gauge.prototype._computeTheme = function (theme) { 88 | if (!theme) { 89 | theme = {} 90 | } 91 | if (typeof theme === 'string') { 92 | theme = this._themes.getTheme(theme) 93 | } else if ( 94 | Object.keys(theme).length === 0 || theme.hasUnicode != null || theme.hasColor != null 95 | ) { 96 | var useUnicode = theme.hasUnicode == null ? hasUnicode() : theme.hasUnicode 97 | var useColor = theme.hasColor == null ? hasColor : theme.hasColor 98 | theme = this._themes.getDefault({ 99 | hasUnicode: useUnicode, 100 | hasColor: useColor, 101 | platform: theme.platform, 102 | }) 103 | } 104 | return theme 105 | } 106 | 107 | Gauge.prototype.setThemeset = function (themes) { 108 | this._themes = themes 109 | this.setTheme(this._theme) 110 | } 111 | 112 | Gauge.prototype.setTheme = function (theme) { 113 | this._gauge.setTheme(this._computeTheme(theme)) 114 | if (this._showing) { 115 | this._requestRedraw() 116 | } 117 | this._theme = theme 118 | } 119 | 120 | Gauge.prototype._requestRedraw = function () { 121 | this._needsRedraw = true 122 | if (!this._fixedFramerate) { 123 | this._doRedraw() 124 | } 125 | } 126 | 127 | Gauge.prototype.getWidth = function () { 128 | return ((this._tty && this._tty.columns) || 80) - 1 129 | } 130 | 131 | Gauge.prototype.setWriteTo = function (writeTo, tty) { 132 | var enabled = !this._disabled 133 | if (enabled) { 134 | this.disable() 135 | } 136 | this._writeTo = writeTo 137 | this._tty = tty || 138 | (writeTo === process.stderr && process.stdout.isTTY && process.stdout) || 139 | (writeTo.isTTY && writeTo) || 140 | this._tty 141 | if (this._gauge) { 142 | this._gauge.setWidth(this.getWidth()) 143 | } 144 | if (enabled) { 145 | this.enable() 146 | } 147 | } 148 | 149 | Gauge.prototype.enable = function () { 150 | if (!this._disabled) { 151 | return 152 | } 153 | this._disabled = false 154 | if (this._tty) { 155 | this._enableEvents() 156 | } 157 | if (this._showing) { 158 | this.show() 159 | } 160 | } 161 | 162 | Gauge.prototype.disable = function () { 163 | if (this._disabled) { 164 | return 165 | } 166 | if (this._showing) { 167 | this._lastUpdateAt = null 168 | this._showing = false 169 | this._doRedraw() 170 | this._showing = true 171 | } 172 | this._disabled = true 173 | if (this._tty) { 174 | this._disableEvents() 175 | } 176 | } 177 | 178 | Gauge.prototype._enableEvents = function () { 179 | if (this._cleanupOnExit) { 180 | this._removeOnExit = onExit(callWith(this, this.disable)) 181 | } 182 | this._tty.on('resize', this._$$handleSizeChange) 183 | if (this._fixedFramerate) { 184 | this.redrawTracker = setInterval(this._$$doRedraw, this._updateInterval) 185 | if (this.redrawTracker.unref) { 186 | this.redrawTracker.unref() 187 | } 188 | } 189 | } 190 | 191 | Gauge.prototype._disableEvents = function () { 192 | this._tty.removeListener('resize', this._$$handleSizeChange) 193 | if (this._fixedFramerate) { 194 | clearInterval(this.redrawTracker) 195 | } 196 | if (this._removeOnExit) { 197 | this._removeOnExit() 198 | } 199 | } 200 | 201 | Gauge.prototype.hide = function (cb) { 202 | if (this._disabled) { 203 | return cb && process.nextTick(cb) 204 | } 205 | if (!this._showing) { 206 | return cb && process.nextTick(cb) 207 | } 208 | this._showing = false 209 | this._doRedraw() 210 | cb && setImmediate(cb) 211 | } 212 | 213 | Gauge.prototype.show = function (section, completed) { 214 | this._showing = true 215 | if (typeof section === 'string') { 216 | this._status.section = section 217 | } else if (typeof section === 'object') { 218 | var sectionKeys = Object.keys(section) 219 | for (var ii = 0; ii < sectionKeys.length; ++ii) { 220 | var key = sectionKeys[ii] 221 | this._status[key] = section[key] 222 | } 223 | } 224 | if (completed != null) { 225 | this._status.completed = completed 226 | } 227 | if (this._disabled) { 228 | return 229 | } 230 | this._requestRedraw() 231 | } 232 | 233 | Gauge.prototype.pulse = function (subsection) { 234 | this._status.subsection = subsection || '' 235 | this._status.spun++ 236 | if (this._disabled) { 237 | return 238 | } 239 | if (!this._showing) { 240 | return 241 | } 242 | this._requestRedraw() 243 | } 244 | 245 | Gauge.prototype._handleSizeChange = function () { 246 | this._gauge.setWidth(this._tty.columns - 1) 247 | this._requestRedraw() 248 | } 249 | 250 | Gauge.prototype._doRedraw = function () { 251 | if (this._disabled || this._paused) { 252 | return 253 | } 254 | if (!this._fixedFramerate) { 255 | var now = Date.now() 256 | if (this._lastUpdateAt && now - this._lastUpdateAt < this._updateInterval) { 257 | return 258 | } 259 | this._lastUpdateAt = now 260 | } 261 | if (!this._showing && this._onScreen) { 262 | this._onScreen = false 263 | var result = this._gauge.hide() 264 | if (this._hideCursor) { 265 | result += this._gauge.showCursor() 266 | } 267 | return this._writeTo.write(result) 268 | } 269 | if (!this._showing && !this._onScreen) { 270 | return 271 | } 272 | if (this._showing && !this._onScreen) { 273 | this._onScreen = true 274 | this._needsRedraw = true 275 | if (this._hideCursor) { 276 | this._writeTo.write(this._gauge.hideCursor()) 277 | } 278 | } 279 | if (!this._needsRedraw) { 280 | return 281 | } 282 | if (!this._writeTo.write(this._gauge.show(this._status))) { 283 | this._paused = true 284 | this._writeTo.on('drain', callWith(this, function () { 285 | this._paused = false 286 | this._doRedraw() 287 | })) 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /lib/plumbing.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var consoleControl = require('console-control-strings') 3 | var renderTemplate = require('./render-template.js') 4 | var validate = require('aproba') 5 | 6 | var Plumbing = module.exports = function (theme, template, width) { 7 | if (!width) { 8 | width = 80 9 | } 10 | validate('OAN', [theme, template, width]) 11 | this.showing = false 12 | this.theme = theme 13 | this.width = width 14 | this.template = template 15 | } 16 | Plumbing.prototype = {} 17 | 18 | Plumbing.prototype.setTheme = function (theme) { 19 | validate('O', [theme]) 20 | this.theme = theme 21 | } 22 | 23 | Plumbing.prototype.setTemplate = function (template) { 24 | validate('A', [template]) 25 | this.template = template 26 | } 27 | 28 | Plumbing.prototype.setWidth = function (width) { 29 | validate('N', [width]) 30 | this.width = width 31 | } 32 | 33 | Plumbing.prototype.hide = function () { 34 | return consoleControl.gotoSOL() + consoleControl.eraseLine() 35 | } 36 | 37 | Plumbing.prototype.hideCursor = consoleControl.hideCursor 38 | 39 | Plumbing.prototype.showCursor = consoleControl.showCursor 40 | 41 | Plumbing.prototype.show = function (status) { 42 | var values = Object.create(this.theme) 43 | for (var key in status) { 44 | values[key] = status[key] 45 | } 46 | 47 | return renderTemplate(this.width, this.template, values).trim() + 48 | consoleControl.color('reset') + 49 | consoleControl.eraseLine() + consoleControl.gotoSOL() 50 | } 51 | -------------------------------------------------------------------------------- /lib/process.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // this exists so we can replace it during testing 3 | module.exports = process 4 | -------------------------------------------------------------------------------- /lib/progress-bar.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var validate = require('aproba') 3 | var renderTemplate = require('./render-template.js') 4 | var wideTruncate = require('./wide-truncate') 5 | var stringWidth = require('string-width') 6 | 7 | module.exports = function (theme, width, completed) { 8 | validate('ONN', [theme, width, completed]) 9 | if (completed < 0) { 10 | completed = 0 11 | } 12 | if (completed > 1) { 13 | completed = 1 14 | } 15 | if (width <= 0) { 16 | return '' 17 | } 18 | var sofar = Math.round(width * completed) 19 | var rest = width - sofar 20 | var template = [ 21 | { type: 'complete', value: repeat(theme.complete, sofar), length: sofar }, 22 | { type: 'remaining', value: repeat(theme.remaining, rest), length: rest }, 23 | ] 24 | return renderTemplate(width, template, theme) 25 | } 26 | 27 | // lodash's way of repeating 28 | function repeat (string, width) { 29 | var result = '' 30 | var n = width 31 | do { 32 | if (n % 2) { 33 | result += string 34 | } 35 | n = Math.floor(n / 2) 36 | /* eslint no-self-assign: 0 */ 37 | string += string 38 | } while (n && stringWidth(result) < width) 39 | 40 | return wideTruncate(result, width) 41 | } 42 | -------------------------------------------------------------------------------- /lib/render-template.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var align = require('wide-align') 3 | var validate = require('aproba') 4 | var wideTruncate = require('./wide-truncate') 5 | var error = require('./error') 6 | var TemplateItem = require('./template-item') 7 | 8 | function renderValueWithValues (values) { 9 | return function (item) { 10 | return renderValue(item, values) 11 | } 12 | } 13 | 14 | var renderTemplate = module.exports = function (width, template, values) { 15 | var items = prepareItems(width, template, values) 16 | var rendered = items.map(renderValueWithValues(values)).join('') 17 | return align.left(wideTruncate(rendered, width), width) 18 | } 19 | 20 | function preType (item) { 21 | var cappedTypeName = item.type[0].toUpperCase() + item.type.slice(1) 22 | return 'pre' + cappedTypeName 23 | } 24 | 25 | function postType (item) { 26 | var cappedTypeName = item.type[0].toUpperCase() + item.type.slice(1) 27 | return 'post' + cappedTypeName 28 | } 29 | 30 | function hasPreOrPost (item, values) { 31 | if (!item.type) { 32 | return 33 | } 34 | return values[preType(item)] || values[postType(item)] 35 | } 36 | 37 | function generatePreAndPost (baseItem, parentValues) { 38 | var item = Object.assign({}, baseItem) 39 | var values = Object.create(parentValues) 40 | var template = [] 41 | var pre = preType(item) 42 | var post = postType(item) 43 | if (values[pre]) { 44 | template.push({ value: values[pre] }) 45 | values[pre] = null 46 | } 47 | item.minLength = null 48 | item.length = null 49 | item.maxLength = null 50 | template.push(item) 51 | values[item.type] = values[item.type] 52 | if (values[post]) { 53 | template.push({ value: values[post] }) 54 | values[post] = null 55 | } 56 | return function ($1, $2, length) { 57 | return renderTemplate(length, template, values) 58 | } 59 | } 60 | 61 | function prepareItems (width, template, values) { 62 | function cloneAndObjectify (item, index, arr) { 63 | var cloned = new TemplateItem(item, width) 64 | var type = cloned.type 65 | if (cloned.value == null) { 66 | if (!(type in values)) { 67 | if (cloned.default == null) { 68 | throw new error.MissingTemplateValue(cloned, values) 69 | } else { 70 | cloned.value = cloned.default 71 | } 72 | } else { 73 | cloned.value = values[type] 74 | } 75 | } 76 | if (cloned.value == null || cloned.value === '') { 77 | return null 78 | } 79 | cloned.index = index 80 | cloned.first = index === 0 81 | cloned.last = index === arr.length - 1 82 | if (hasPreOrPost(cloned, values)) { 83 | cloned.value = generatePreAndPost(cloned, values) 84 | } 85 | return cloned 86 | } 87 | 88 | var output = template.map(cloneAndObjectify).filter(function (item) { 89 | return item != null 90 | }) 91 | 92 | var remainingSpace = width 93 | var variableCount = output.length 94 | 95 | function consumeSpace (length) { 96 | if (length > remainingSpace) { 97 | length = remainingSpace 98 | } 99 | remainingSpace -= length 100 | } 101 | 102 | function finishSizing (item, length) { 103 | if (item.finished) { 104 | throw new error.Internal('Tried to finish template item that was already finished') 105 | } 106 | if (length === Infinity) { 107 | throw new error.Internal('Length of template item cannot be infinity') 108 | } 109 | if (length != null) { 110 | item.length = length 111 | } 112 | item.minLength = null 113 | item.maxLength = null 114 | --variableCount 115 | item.finished = true 116 | if (item.length == null) { 117 | item.length = item.getBaseLength() 118 | } 119 | if (item.length == null) { 120 | throw new error.Internal('Finished template items must have a length') 121 | } 122 | consumeSpace(item.getLength()) 123 | } 124 | 125 | output.forEach(function (item) { 126 | if (!item.kerning) { 127 | return 128 | } 129 | var prevPadRight = item.first ? 0 : output[item.index - 1].padRight 130 | if (!item.first && prevPadRight < item.kerning) { 131 | item.padLeft = item.kerning - prevPadRight 132 | } 133 | if (!item.last) { 134 | item.padRight = item.kerning 135 | } 136 | }) 137 | 138 | // Finish any that have a fixed (literal or intuited) length 139 | output.forEach(function (item) { 140 | if (item.getBaseLength() == null) { 141 | return 142 | } 143 | finishSizing(item) 144 | }) 145 | 146 | var resized = 0 147 | var resizing 148 | var hunkSize 149 | do { 150 | resizing = false 151 | hunkSize = Math.round(remainingSpace / variableCount) 152 | output.forEach(function (item) { 153 | if (item.finished) { 154 | return 155 | } 156 | if (!item.maxLength) { 157 | return 158 | } 159 | if (item.getMaxLength() < hunkSize) { 160 | finishSizing(item, item.maxLength) 161 | resizing = true 162 | } 163 | }) 164 | } while (resizing && resized++ < output.length) 165 | if (resizing) { 166 | throw new error.Internal('Resize loop iterated too many times while determining maxLength') 167 | } 168 | 169 | resized = 0 170 | do { 171 | resizing = false 172 | hunkSize = Math.round(remainingSpace / variableCount) 173 | output.forEach(function (item) { 174 | if (item.finished) { 175 | return 176 | } 177 | if (!item.minLength) { 178 | return 179 | } 180 | if (item.getMinLength() >= hunkSize) { 181 | finishSizing(item, item.minLength) 182 | resizing = true 183 | } 184 | }) 185 | } while (resizing && resized++ < output.length) 186 | if (resizing) { 187 | throw new error.Internal('Resize loop iterated too many times while determining minLength') 188 | } 189 | 190 | hunkSize = Math.round(remainingSpace / variableCount) 191 | output.forEach(function (item) { 192 | if (item.finished) { 193 | return 194 | } 195 | finishSizing(item, hunkSize) 196 | }) 197 | 198 | return output 199 | } 200 | 201 | function renderFunction (item, values, length) { 202 | validate('OON', arguments) 203 | if (item.type) { 204 | return item.value(values, values[item.type + 'Theme'] || {}, length) 205 | } else { 206 | return item.value(values, {}, length) 207 | } 208 | } 209 | 210 | function renderValue (item, values) { 211 | var length = item.getBaseLength() 212 | var value = typeof item.value === 'function' ? renderFunction(item, values, length) : item.value 213 | if (value == null || value === '') { 214 | return '' 215 | } 216 | var alignWith = align[item.align] || align.left 217 | var leftPadding = item.padLeft ? align.left('', item.padLeft) : '' 218 | var rightPadding = item.padRight ? align.right('', item.padRight) : '' 219 | var truncated = wideTruncate(String(value), length) 220 | var aligned = alignWith(truncated, length) 221 | return leftPadding + aligned + rightPadding 222 | } 223 | -------------------------------------------------------------------------------- /lib/set-immediate.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var process = require('./process') 3 | try { 4 | module.exports = setImmediate 5 | } catch (ex) { 6 | module.exports = process.nextTick 7 | } 8 | -------------------------------------------------------------------------------- /lib/set-interval.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // this exists so we can replace it during testing 3 | module.exports = setInterval 4 | -------------------------------------------------------------------------------- /lib/spin.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function spin (spinstr, spun) { 4 | return spinstr[spun % spinstr.length] 5 | } 6 | -------------------------------------------------------------------------------- /lib/template-item.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var stringWidth = require('string-width') 3 | 4 | module.exports = TemplateItem 5 | 6 | function isPercent (num) { 7 | if (typeof num !== 'string') { 8 | return false 9 | } 10 | return num.slice(-1) === '%' 11 | } 12 | 13 | function percent (num) { 14 | return Number(num.slice(0, -1)) / 100 15 | } 16 | 17 | function TemplateItem (values, outputLength) { 18 | this.overallOutputLength = outputLength 19 | this.finished = false 20 | this.type = null 21 | this.value = null 22 | this.length = null 23 | this.maxLength = null 24 | this.minLength = null 25 | this.kerning = null 26 | this.align = 'left' 27 | this.padLeft = 0 28 | this.padRight = 0 29 | this.index = null 30 | this.first = null 31 | this.last = null 32 | if (typeof values === 'string') { 33 | this.value = values 34 | } else { 35 | for (var prop in values) { 36 | this[prop] = values[prop] 37 | } 38 | } 39 | // Realize percents 40 | if (isPercent(this.length)) { 41 | this.length = Math.round(this.overallOutputLength * percent(this.length)) 42 | } 43 | if (isPercent(this.minLength)) { 44 | this.minLength = Math.round(this.overallOutputLength * percent(this.minLength)) 45 | } 46 | if (isPercent(this.maxLength)) { 47 | this.maxLength = Math.round(this.overallOutputLength * percent(this.maxLength)) 48 | } 49 | return this 50 | } 51 | 52 | TemplateItem.prototype = {} 53 | 54 | TemplateItem.prototype.getBaseLength = function () { 55 | var length = this.length 56 | if ( 57 | length == null && 58 | typeof this.value === 'string' && 59 | this.maxLength == null && 60 | this.minLength == null 61 | ) { 62 | length = stringWidth(this.value) 63 | } 64 | return length 65 | } 66 | 67 | TemplateItem.prototype.getLength = function () { 68 | var length = this.getBaseLength() 69 | if (length == null) { 70 | return null 71 | } 72 | return length + this.padLeft + this.padRight 73 | } 74 | 75 | TemplateItem.prototype.getMaxLength = function () { 76 | if (this.maxLength == null) { 77 | return null 78 | } 79 | return this.maxLength + this.padLeft + this.padRight 80 | } 81 | 82 | TemplateItem.prototype.getMinLength = function () { 83 | if (this.minLength == null) { 84 | return null 85 | } 86 | return this.minLength + this.padLeft + this.padRight 87 | } 88 | -------------------------------------------------------------------------------- /lib/theme-set.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function () { 4 | return ThemeSetProto.newThemeSet() 5 | } 6 | 7 | var ThemeSetProto = {} 8 | 9 | ThemeSetProto.baseTheme = require('./base-theme.js') 10 | 11 | ThemeSetProto.newTheme = function (parent, theme) { 12 | if (!theme) { 13 | theme = parent 14 | parent = this.baseTheme 15 | } 16 | return Object.assign({}, parent, theme) 17 | } 18 | 19 | ThemeSetProto.getThemeNames = function () { 20 | return Object.keys(this.themes) 21 | } 22 | 23 | ThemeSetProto.addTheme = function (name, parent, theme) { 24 | this.themes[name] = this.newTheme(parent, theme) 25 | } 26 | 27 | ThemeSetProto.addToAllThemes = function (theme) { 28 | var themes = this.themes 29 | Object.keys(themes).forEach(function (name) { 30 | Object.assign(themes[name], theme) 31 | }) 32 | Object.assign(this.baseTheme, theme) 33 | } 34 | 35 | ThemeSetProto.getTheme = function (name) { 36 | if (!this.themes[name]) { 37 | throw this.newMissingThemeError(name) 38 | } 39 | return this.themes[name] 40 | } 41 | 42 | ThemeSetProto.setDefault = function (opts, name) { 43 | if (name == null) { 44 | name = opts 45 | opts = {} 46 | } 47 | var platform = opts.platform == null ? 'fallback' : opts.platform 48 | var hasUnicode = !!opts.hasUnicode 49 | var hasColor = !!opts.hasColor 50 | if (!this.defaults[platform]) { 51 | this.defaults[platform] = { true: {}, false: {} } 52 | } 53 | this.defaults[platform][hasUnicode][hasColor] = name 54 | } 55 | 56 | ThemeSetProto.getDefault = function (opts) { 57 | if (!opts) { 58 | opts = {} 59 | } 60 | var platformName = opts.platform || process.platform 61 | var platform = this.defaults[platformName] || this.defaults.fallback 62 | var hasUnicode = !!opts.hasUnicode 63 | var hasColor = !!opts.hasColor 64 | if (!platform) { 65 | throw this.newMissingDefaultThemeError(platformName, hasUnicode, hasColor) 66 | } 67 | if (!platform[hasUnicode][hasColor]) { 68 | if (hasUnicode && hasColor && platform[!hasUnicode][hasColor]) { 69 | hasUnicode = false 70 | } else if (hasUnicode && hasColor && platform[hasUnicode][!hasColor]) { 71 | hasColor = false 72 | } else if (hasUnicode && hasColor && platform[!hasUnicode][!hasColor]) { 73 | hasUnicode = false 74 | hasColor = false 75 | } else if (hasUnicode && !hasColor && platform[!hasUnicode][hasColor]) { 76 | hasUnicode = false 77 | } else if (!hasUnicode && hasColor && platform[hasUnicode][!hasColor]) { 78 | hasColor = false 79 | } else if (platform === this.defaults.fallback) { 80 | throw this.newMissingDefaultThemeError(platformName, hasUnicode, hasColor) 81 | } 82 | } 83 | if (platform[hasUnicode][hasColor]) { 84 | return this.getTheme(platform[hasUnicode][hasColor]) 85 | } else { 86 | return this.getDefault(Object.assign({}, opts, { platform: 'fallback' })) 87 | } 88 | } 89 | 90 | ThemeSetProto.newMissingThemeError = function newMissingThemeError (name) { 91 | var err = new Error('Could not find a gauge theme named "' + name + '"') 92 | Error.captureStackTrace.call(err, newMissingThemeError) 93 | err.theme = name 94 | err.code = 'EMISSINGTHEME' 95 | return err 96 | } 97 | 98 | ThemeSetProto.newMissingDefaultThemeError = 99 | function newMissingDefaultThemeError (platformName, hasUnicode, hasColor) { 100 | var err = new Error( 101 | 'Could not find a gauge theme for your platform/unicode/color use combo:\n' + 102 | ' platform = ' + platformName + '\n' + 103 | ' hasUnicode = ' + hasUnicode + '\n' + 104 | ' hasColor = ' + hasColor) 105 | Error.captureStackTrace.call(err, newMissingDefaultThemeError) 106 | err.platform = platformName 107 | err.hasUnicode = hasUnicode 108 | err.hasColor = hasColor 109 | err.code = 'EMISSINGTHEME' 110 | return err 111 | } 112 | 113 | ThemeSetProto.newThemeSet = function () { 114 | var themeset = function (opts) { 115 | return themeset.getDefault(opts) 116 | } 117 | return Object.assign(themeset, ThemeSetProto, { 118 | themes: Object.assign({}, this.themes), 119 | baseTheme: Object.assign({}, this.baseTheme), 120 | defaults: JSON.parse(JSON.stringify(this.defaults || {})), 121 | }) 122 | } 123 | -------------------------------------------------------------------------------- /lib/themes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var color = require('console-control-strings').color 3 | var ThemeSet = require('./theme-set.js') 4 | 5 | var themes = module.exports = new ThemeSet() 6 | 7 | themes.addTheme('ASCII', { 8 | preProgressbar: '[', 9 | postProgressbar: ']', 10 | progressbarTheme: { 11 | complete: '#', 12 | remaining: '.', 13 | }, 14 | activityIndicatorTheme: '-\\|/', 15 | preSubsection: '>', 16 | }) 17 | 18 | themes.addTheme('colorASCII', themes.getTheme('ASCII'), { 19 | progressbarTheme: { 20 | preComplete: color('bgBrightWhite', 'brightWhite'), 21 | complete: '#', 22 | postComplete: color('reset'), 23 | preRemaining: color('bgBrightBlack', 'brightBlack'), 24 | remaining: '.', 25 | postRemaining: color('reset'), 26 | }, 27 | }) 28 | 29 | themes.addTheme('brailleSpinner', { 30 | preProgressbar: '(', 31 | postProgressbar: ')', 32 | progressbarTheme: { 33 | complete: '#', 34 | remaining: '⠂', 35 | }, 36 | activityIndicatorTheme: '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏', 37 | preSubsection: '>', 38 | }) 39 | 40 | themes.addTheme('colorBrailleSpinner', themes.getTheme('brailleSpinner'), { 41 | progressbarTheme: { 42 | preComplete: color('bgBrightWhite', 'brightWhite'), 43 | complete: '#', 44 | postComplete: color('reset'), 45 | preRemaining: color('bgBrightBlack', 'brightBlack'), 46 | remaining: '⠂', 47 | postRemaining: color('reset'), 48 | }, 49 | }) 50 | 51 | themes.setDefault({}, 'ASCII') 52 | themes.setDefault({ hasColor: true }, 'colorASCII') 53 | themes.setDefault({ platform: 'darwin', hasUnicode: true }, 'brailleSpinner') 54 | themes.setDefault({ platform: 'darwin', hasUnicode: true, hasColor: true }, 'colorBrailleSpinner') 55 | themes.setDefault({ platform: 'linux', hasUnicode: true }, 'brailleSpinner') 56 | themes.setDefault({ platform: 'linux', hasUnicode: true, hasColor: true }, 'colorBrailleSpinner') 57 | -------------------------------------------------------------------------------- /lib/wide-truncate.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var stringWidth = require('string-width') 3 | var stripAnsi = require('strip-ansi') 4 | 5 | module.exports = wideTruncate 6 | 7 | function wideTruncate (str, target) { 8 | if (stringWidth(str) === 0) { 9 | return str 10 | } 11 | if (target <= 0) { 12 | return '' 13 | } 14 | if (stringWidth(str) <= target) { 15 | return str 16 | } 17 | 18 | // We compute the number of bytes of ansi sequences here and add 19 | // that to our initial truncation to ensure that we don't slice one 20 | // that we want to keep in half. 21 | var noAnsi = stripAnsi(str) 22 | var ansiSize = str.length + noAnsi.length 23 | var truncated = str.slice(0, target + ansiSize) 24 | 25 | // we have to shrink the result to account for our ansi sequence buffer 26 | // (if an ansi sequence was truncated) and double width characters. 27 | while (stringWidth(truncated) > target) { 28 | truncated = truncated.slice(0, -1) 29 | } 30 | return truncated 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gauge", 3 | "version": "5.0.2", 4 | "description": "A terminal based horizontal gauge", 5 | "main": "lib", 6 | "scripts": { 7 | "test": "tap", 8 | "lint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\"", 9 | "postlint": "template-oss-check", 10 | "lintfix": "npm run lint -- --fix", 11 | "snap": "tap", 12 | "posttest": "npm run lint", 13 | "template-oss-apply": "template-oss-apply --force" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/npm/gauge.git" 18 | }, 19 | "keywords": [ 20 | "progressbar", 21 | "progress", 22 | "gauge" 23 | ], 24 | "author": "GitHub Inc.", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/npm/gauge/issues" 28 | }, 29 | "homepage": "https://github.com/npm/gauge", 30 | "dependencies": { 31 | "aproba": "^1.0.3 || ^2.0.0", 32 | "color-support": "^1.1.3", 33 | "console-control-strings": "^1.1.0", 34 | "has-unicode": "^2.0.1", 35 | "signal-exit": "^4.0.1", 36 | "string-width": "^4.2.3", 37 | "strip-ansi": "^6.0.1", 38 | "wide-align": "^1.1.5" 39 | }, 40 | "devDependencies": { 41 | "@npmcli/eslint-config": "^4.0.0", 42 | "@npmcli/template-oss": "4.22.0", 43 | "readable-stream": "^4.0.0", 44 | "tap": "^16.0.1" 45 | }, 46 | "files": [ 47 | "bin/", 48 | "lib/" 49 | ], 50 | "engines": { 51 | "node": "^14.17.0 || ^16.13.0 || >=18.0.0" 52 | }, 53 | "tap": { 54 | "branches": 79, 55 | "statements": 89, 56 | "functions": 92, 57 | "lines": 90, 58 | "nyc-arg": [ 59 | "--exclude", 60 | "tap-snapshots/**" 61 | ] 62 | }, 63 | "templateOSS": { 64 | "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", 65 | "version": "4.22.0", 66 | "publish": "true" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "group-pull-request-title-pattern": "chore: release ${version}", 3 | "pull-request-title-pattern": "chore: release${component} ${version}", 4 | "changelog-sections": [ 5 | { 6 | "type": "feat", 7 | "section": "Features", 8 | "hidden": false 9 | }, 10 | { 11 | "type": "fix", 12 | "section": "Bug Fixes", 13 | "hidden": false 14 | }, 15 | { 16 | "type": "docs", 17 | "section": "Documentation", 18 | "hidden": false 19 | }, 20 | { 21 | "type": "deps", 22 | "section": "Dependencies", 23 | "hidden": false 24 | }, 25 | { 26 | "type": "chore", 27 | "section": "Chores", 28 | "hidden": true 29 | } 30 | ], 31 | "packages": { 32 | ".": { 33 | "package-name": "" 34 | } 35 | }, 36 | "prerelease-type": "pre" 37 | } 38 | -------------------------------------------------------------------------------- /test/base-theme.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const t = require('tap') 3 | const baseTheme = t.mock('../lib/base-theme.js', { 4 | '../lib/spin.js': function (theme, spun) { 5 | return [theme, spun] 6 | }, 7 | '../lib/progress-bar.js': function (theme, width, completed) { 8 | return [theme, width, completed] 9 | }, 10 | }) 11 | 12 | t.test('activityIndicator', async t => { 13 | t.equal(baseTheme.activityIndicator({}, {}, 80), undefined, 'no spun') 14 | t.strictSame( 15 | baseTheme.activityIndicator({ spun: 3 }, { me: true }, 9999), 16 | [{ me: true }, 3], 17 | 'spun' 18 | ) 19 | }) 20 | 21 | t.test('progressBar', async t => { 22 | t.equal(baseTheme.progressbar({}, {}, 80), undefined, 'no completion') 23 | t.strictSame( 24 | baseTheme.progressbar({ completed: 33 }, { me: true }, 100), 25 | [{ me: true }, 100, 33], 26 | 'completion!' 27 | ) 28 | }) 29 | -------------------------------------------------------------------------------- /test/error.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const t = require('tap') 3 | const error = require('../lib/error.js') 4 | 5 | t.test('User', async t => { 6 | var msg = 'example' 7 | var user = new error.User(msg) 8 | t.ok(user instanceof Error, 'isa Error') 9 | t.equal(user.code, 'EGAUGE', 'code') 10 | t.equal(user.message, msg, 'maintained message') 11 | }) 12 | 13 | t.test('MissingTemplateValue', async t => { 14 | var item = { type: 'abc' } 15 | var values = { abc: 'def', ghi: 'jkl' } 16 | var user = new error.MissingTemplateValue(item, values) 17 | t.ok(user instanceof Error, 'isa Error') 18 | t.equal(user.code, 'EGAUGE', 'code') 19 | t.match(user.message, new RegExp(item.type), 'contains type') 20 | t.strictSame(user.template, item, 'passed through template item') 21 | t.strictSame(user.values, values, 'passed through values') 22 | }) 23 | 24 | t.test('Internal', async t => { 25 | var msg = 'example' 26 | var user = new error.Internal(msg) 27 | t.ok(user instanceof Error, 'isa Error') 28 | t.equal(user.code, 'EGAUGEINTERNAL', 'code') 29 | t.equal(user.message, msg, 'maintained message') 30 | }) 31 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const t = require('tap') 3 | const test = require('tap').test 4 | const Gauge = require('..') 5 | const stream = require('readable-stream') 6 | const util = require('util') 7 | const EventEmitter = require('events').EventEmitter 8 | 9 | function Sink () { 10 | stream.Writable.call(this, arguments) 11 | } 12 | util.inherits(Sink, stream.Writable) 13 | Sink.prototype._write = function (data, enc, cb) { 14 | cb() 15 | } 16 | 17 | const results = new EventEmitter() 18 | function MockPlumbing (theme, template, columns) { 19 | results.theme = theme 20 | results.template = template 21 | results.columns = columns 22 | results.emit('new', theme, template, columns) 23 | } 24 | MockPlumbing.prototype = {} 25 | 26 | function RecordCall (name) { 27 | return function () { 28 | const args = Array.prototype.slice.call(arguments) 29 | results.emit('called', [name, args]) 30 | results.emit('called:' + name, args) 31 | return '' 32 | } 33 | } 34 | 35 | ['setTheme', 'setTemplate', 'setWidth', 'hide', 'show', 'hideCursor', 'showCursor'].forEach( 36 | function (fn) { 37 | MockPlumbing.prototype[fn] = RecordCall(fn) 38 | } 39 | ) 40 | 41 | t.test('defaults', async t => { 42 | let gauge = new Gauge(process.stdout) 43 | t.equal(gauge._disabled, !process.stdout.isTTY, 'disabled') 44 | t.equal(gauge._updateInterval, 50, 'updateInterval') 45 | if (process.stdout.isTTY) { 46 | t.equal(gauge._tty, process.stdout, 'tty') 47 | gauge.disable() 48 | gauge = new Gauge(process.stderr) 49 | t.equal(gauge._tty, process.stdout, 'tty is stdout when writeTo is stderr') 50 | } 51 | gauge.disable() 52 | gauge = new Gauge(new Sink()) 53 | t.equal(gauge._tty, undefined, 'non-tty stream is not tty') 54 | gauge.disable() 55 | }) 56 | 57 | t.test('construct', async t => { 58 | const output = new Sink() 59 | output.isTTY = true 60 | output.columns = 16 61 | const gauge = new Gauge(output, { 62 | Plumbing: MockPlumbing, 63 | theme: ['THEME'], 64 | template: ['TEMPLATE'], 65 | enabled: false, 66 | updateInterval: 0, 67 | fixedFramerate: false, 68 | }) 69 | t.ok(gauge) 70 | t.equal(results.columns, 15, 'width passed through') 71 | t.same(results.theme, ['THEME'], 'theme passed through') 72 | t.same(results.template, ['TEMPLATE'], 'template passed through') 73 | t.equal(gauge.isEnabled(), false, 'disabled') 74 | }) 75 | 76 | t.test('show & pulse: fixedframerate', t => { 77 | t.plan(3) 78 | // this helps us abort if something never emits an event 79 | // it also keeps things alive long enough to actually get output =D 80 | const testtimeout = setTimeout(function () { 81 | t.end() 82 | }, 1000) 83 | const output = new Sink() 84 | output.isTTY = true 85 | output.columns = 16 86 | const gauge = new Gauge(output, { 87 | Plumbing: MockPlumbing, 88 | updateInterval: 10, 89 | fixedFramerate: true, 90 | }) 91 | gauge.show('NAME', 0.1) 92 | results.once('called:show', checkBasicShow) 93 | function checkBasicShow (args) { 94 | t.strictSame( 95 | args, 96 | [{ spun: 0, section: 'NAME', subsection: '', completed: 0.1 }], 97 | 'check basic show' 98 | ) 99 | 100 | gauge.show('S') 101 | gauge.pulse() 102 | results.once('called:show', checkPulse) 103 | } 104 | function checkPulse (args) { 105 | t.strictSame(args, [ 106 | { spun: 1, section: 'S', subsection: '', completed: 0.1 }, 107 | ], 'check pulse') 108 | 109 | gauge.pulse('P') 110 | results.once('called:show', checkPulseWithArg) 111 | } 112 | function checkPulseWithArg (args) { 113 | t.strictSame(args, [ 114 | { spun: 2, section: 'S', subsection: 'P', completed: 0.1 }, 115 | ], 'check pulse w/ arg') 116 | 117 | gauge.disable() 118 | clearTimeout(testtimeout) 119 | t.end() 120 | } 121 | }) 122 | 123 | t.test('window resizing', t => { 124 | const testtimeout = setTimeout(function () { 125 | t.end() 126 | }, 1000) 127 | const output = new Sink() 128 | output.isTTY = true 129 | output.columns = 32 130 | 131 | const gauge = new Gauge(output, { 132 | Plumbing: MockPlumbing, 133 | updateInterval: 0, 134 | fixedFramerate: true, 135 | }) 136 | gauge.show('NAME', 0.1) 137 | 138 | results.once('called:show', function (args) { 139 | t.strictSame(args, [{ 140 | section: 'NAME', 141 | subsection: '', 142 | completed: 0.1, 143 | spun: 0, 144 | }]) 145 | 146 | results.once('called:setWidth', lookForResize) 147 | 148 | output.columns = 16 149 | output.emit('resize') 150 | gauge.show('NAME', 0.5) 151 | }) 152 | function lookForResize (args) { 153 | t.strictSame(args, [15]) 154 | results.once('called:show', lookForShow) 155 | } 156 | function lookForShow (args) { 157 | t.strictSame(args, [{ 158 | section: 'NAME', 159 | subsection: '', 160 | completed: 0.5, 161 | spun: 0, 162 | }]) 163 | gauge.disable() 164 | clearTimeout(testtimeout) 165 | t.end() 166 | } 167 | }) 168 | 169 | function collectResults (time, cb) { 170 | const collected = [] 171 | function collect (called) { 172 | collected.push(called) 173 | } 174 | results.on('called', collect) 175 | setTimeout(function () { 176 | results.removeListener('called', collect) 177 | cb(collected) 178 | }, time) 179 | } 180 | 181 | t.test('hideCursor:true', t => { 182 | const output = new Sink() 183 | output.isTTY = true 184 | output.columns = 16 185 | const gauge = new Gauge(output, { 186 | Plumbing: MockPlumbing, 187 | theme: ['THEME'], 188 | template: ['TEMPLATE'], 189 | enabled: true, 190 | updateInterval: 90, 191 | fixedFramerate: true, 192 | hideCursor: true, 193 | }) 194 | collectResults(100, andCursorHidden) 195 | gauge.show('NAME', 0.5) 196 | t.equal(gauge.isEnabled(), true, 'enabled') 197 | function andCursorHidden (got) { 198 | const expected = [ 199 | ['hideCursor', []], 200 | ['show', [{ 201 | spun: 0, 202 | section: 'NAME', 203 | subsection: '', 204 | completed: 0.5, 205 | }]], 206 | ] 207 | t.strictSame(got, expected, 'hideCursor') 208 | gauge.disable() 209 | t.end() 210 | } 211 | }) 212 | 213 | test('hideCursor:false', t => { 214 | const output = new Sink() 215 | output.isTTY = true 216 | output.columns = 16 217 | const gauge = new Gauge(output, { 218 | Plumbing: MockPlumbing, 219 | theme: ['THEME'], 220 | template: ['TEMPLATE'], 221 | enabled: true, 222 | updateInterval: 90, 223 | fixedFramerate: true, 224 | hideCursor: false, 225 | }) 226 | collectResults(100, andCursorHidden) 227 | gauge.show('NAME', 0.5) 228 | function andCursorHidden (got) { 229 | const expected = [ 230 | ['show', [{ 231 | spun: 0, 232 | section: 'NAME', 233 | subsection: '', 234 | completed: 0.5, 235 | }]], 236 | ] 237 | t.strictSame(got, expected, 'do not hideCursor') 238 | gauge.disable() 239 | t.end() 240 | } 241 | }) 242 | 243 | // [> todo missing: 244 | 245 | // constructor 246 | // arg2 is writeTo, arg1 is opts 247 | // arg2 is writeTo, arg1 is null 248 | // no args, all defaults 249 | 250 | // setTemplate 251 | // setThemeset 252 | // setTheme 253 | // w/ theme selector 254 | // w/ theme name 255 | // w/ theme object 256 | // setWriteTo 257 | // while enabled/disabled 258 | // w/ tty 259 | // w/o tty & writeTo = process.stderr & process.stdout isTTY 260 | // w/o tty & writeTo = process.stderr & process.stdout !isTTY 261 | // enable 262 | // w/ _showing = true 263 | // hide 264 | // w/ disabled 265 | // w/ !disabled & !showing 266 | // w/ !disabled & showing 267 | // w/ these & cb 268 | // show 269 | // w/ disabled 270 | // w/ object arg1 271 | // pulse 272 | // w/ disabled 273 | // w/ !showing 274 | 275 | // anything to do with _fixedFramerate 276 | 277 | // trigger _doRedraw 278 | // w/o showing & w/o _onScreen (eg, hide, show, hide, I think) 279 | // w/o _needsRedraw 280 | 281 | // Everything to do with back pressure from _writeTo 282 | 283 | // */ 284 | -------------------------------------------------------------------------------- /test/plumbing.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const t = require('tap') 3 | const Plumbing = t.mock('../lib/plumbing.js', { 4 | '../lib/render-template.js': function (width, template, values) { 5 | if (values.x) { 6 | values.x = values.x 7 | } // pull in from parent object for stringify 8 | return 'w:' + width + ', t:' + JSON.stringify(template) + ', v:' + JSON.stringify(values) 9 | }, 10 | 'console-control-strings': { 11 | eraseLine: function () { 12 | return 'ERASE' 13 | }, 14 | gotoSOL: function () { 15 | return 'CR' 16 | }, 17 | color: function (to) { 18 | return 'COLOR:' + to 19 | }, 20 | hideCursor: function () { 21 | return 'HIDE' 22 | }, 23 | showCursor: function () { 24 | return 'SHOW' 25 | }, 26 | }, 27 | }) 28 | 29 | const template = [ 30 | { type: 'name' }, 31 | ] 32 | const theme = {} 33 | const plumbing = new Plumbing(theme, template, 10) 34 | 35 | // These three produce fixed strings and are entirely static, so as long as 36 | // they produce _something_ they're probably ok. Actually testing them will 37 | // require something that understands ansi codes. 38 | t.test('showCursor', function (t) { 39 | t.equal(plumbing.showCursor(), 'SHOW') 40 | t.end() 41 | }) 42 | t.test('hideCursor', function (t) { 43 | t.equal(plumbing.hideCursor(), 'HIDE') 44 | t.end() 45 | }) 46 | t.test('hide', function (t) { 47 | t.equal(plumbing.hide(), 'CRERASE') 48 | t.end() 49 | }) 50 | 51 | t.test('show', function (t) { 52 | t.equal( 53 | plumbing.show({ name: 'test' }), 54 | 'w:10, t:[{"type":"name"}], v:{"name":"test"}COLOR:resetERASECR' 55 | ) 56 | t.end() 57 | }) 58 | 59 | t.test('width', function (t) { 60 | const defaultWidth = new Plumbing(theme, template) 61 | t.equal( 62 | defaultWidth.show({ name: 'test' }), 63 | 'w:80, t:[{"type":"name"}], v:{"name":"test"}COLOR:resetERASECR' 64 | ) 65 | t.end() 66 | }) 67 | 68 | t.test('setTheme', function (t) { 69 | plumbing.setTheme({ x: 'abc' }) 70 | t.equal( 71 | plumbing.show( 72 | { name: 'test' }), 73 | 'w:10, t:[{"type":"name"}], v:{"name":"test","x":"abc"}COLOR:resetERASECR' 74 | ) 75 | t.end() 76 | }) 77 | 78 | t.test('setTemplate', function (t) { 79 | plumbing.setTemplate([{ type: 'name' }, { type: 'x' }]) 80 | t.equal( 81 | plumbing.show({ name: 'test' }), 82 | 'w:10, t:[{"type":"name"},{"type":"x"}], v:{"name":"test","x":"abc"}COLOR:resetERASECR' 83 | ) 84 | t.end() 85 | }) 86 | 87 | t.test('setWidth', function (t) { 88 | plumbing.setWidth(20) 89 | t.equal( 90 | plumbing.show({ name: 'test' }), 91 | 'w:20, t:[{"type":"name"},{"type":"x"}], v:{"name":"test","x":"abc"}COLOR:resetERASECR' 92 | ) 93 | t.end() 94 | }) 95 | -------------------------------------------------------------------------------- /test/progress-bar.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const t = require('tap') 3 | const progressBar = require('../lib/progress-bar') 4 | 5 | t.test('progressBar', function (t) { 6 | var theme = { 7 | complete: '#', 8 | remaining: '-', 9 | } 10 | var result 11 | result = progressBar(theme, 10, 0) 12 | t.equal(result, '----------', '0% bar') 13 | result = progressBar(theme, 10, 0.5) 14 | t.equal(result, '#####-----', '50% bar') 15 | result = progressBar(theme, 10, 1) 16 | t.equal(result, '##########', '100% bar') 17 | result = progressBar(theme, 10, -100) 18 | t.equal(result, '----------', '0% underflow bar') 19 | result = progressBar(theme, 10, 100) 20 | t.equal(result, '##########', '100% overflow bar') 21 | result = progressBar(theme, 0, 0.5) 22 | t.equal(result, '', '0 width bar') 23 | 24 | var multicharTheme = { 25 | complete: '123', 26 | remaining: 'abc', 27 | } 28 | result = progressBar(multicharTheme, 10, 0) 29 | t.equal(result, 'abcabcabca', '0% bar') 30 | result = progressBar(multicharTheme, 10, 0.5) 31 | t.equal(result, '12312abcab', '50% bar') 32 | result = progressBar(multicharTheme, 10, 1) 33 | t.equal(result, '1231231231', '100% bar') 34 | result = progressBar(multicharTheme, 10, -100) 35 | t.equal(result, 'abcabcabca', '0% underflow bar') 36 | result = progressBar(multicharTheme, 10, 100) 37 | t.equal(result, '1231231231', '100% overflow bar') 38 | result = progressBar(multicharTheme, 0, 0.5) 39 | t.equal(result, '', '0 width bar') 40 | 41 | t.end() 42 | }) 43 | -------------------------------------------------------------------------------- /test/render-template.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const t = require('tap') 3 | const renderTemplate = require('../lib/render-template') 4 | 5 | t.test('renderTemplate', function (t) { 6 | var result 7 | result = renderTemplate(10, [{ type: 'name' }], { name: 'NAME' }) 8 | t.equal(result, 'NAME ', 'name substitution') 9 | 10 | result = renderTemplate(10, 11 | [{ type: 'name' }, { type: 'completionbar' }], 12 | { 13 | name: 'NAME', 14 | completionbar: function (values, theme, width) { 15 | return 'xx' + String(width) + 'xx' 16 | }, 17 | }) 18 | t.equal(result, 'NAMExx6xx ', 'name + 50%') 19 | 20 | result = renderTemplate(10, ['static'], {}) 21 | t.equal(result, 'static ', 'static text') 22 | 23 | result = renderTemplate(10, ['static', { type: 'name' }], { name: 'NAME' }) 24 | t.equal(result, 'staticNAME', 'static text + var') 25 | 26 | result = renderTemplate(10, ['static', { type: 'name', kerning: 1 }], { name: 'NAME' }) 27 | t.equal(result, 'static NAM', 'pre-separated') 28 | 29 | result = renderTemplate(10, [{ type: 'name', kerning: 1 }, 'static'], { name: 'NAME' }) 30 | t.equal(result, 'NAME stati', 'post-separated') 31 | 32 | result = renderTemplate(10, ['1', { type: 'name', kerning: 1 }, '2'], { name: '' }) 33 | t.equal(result, '12 ', 'separated no value') 34 | 35 | result = renderTemplate(10, ['1', { type: 'name', kerning: 1 }, '2'], { name: 'NAME' }) 36 | t.equal(result, '1 NAME 2 ', 'separated value') 37 | 38 | result = renderTemplate( 39 | 10, 40 | ['AB', { type: 'name', kerning: 1 }, { value: 'CD', kerning: 1 }], 41 | { name: 'NAME' } 42 | ) 43 | t.equal(result, 'AB NAME CD', 'multi kerning') 44 | 45 | result = renderTemplate(10, [{ type: 'name', length: '50%' }, 'static'], { name: 'N' }) 46 | t.equal(result, 'N stati', 'percent length') 47 | 48 | try { 49 | result = renderTemplate(10, [{ type: 'xyzzy' }, 'static'], {}) 50 | t.fail('missing type') 51 | } catch (e) { 52 | t.pass('missing type') 53 | } 54 | 55 | result = 56 | renderTemplate(10, [{ type: 'name', minLength: '20%' }, 'this long thing'], { name: 'N' }) 57 | t.equal(result, 'N this lon', 'percent minlength') 58 | 59 | result = renderTemplate(10, [{ type: 'name', maxLength: '20%' }, 'nope'], { name: 'NAME' }) 60 | t.equal(result, 'NAnope ', 'percent maxlength') 61 | 62 | result = renderTemplate(10, [{ type: 'name', padLeft: 2, padRight: 2 }, '||'], { name: 'NAME' }) 63 | t.equal(result, ' NAME ||', 'manual padding') 64 | 65 | result = renderTemplate(10, [{ value: 'ABC', minLength: 2, maxLength: 6 }, 'static'], {}) 66 | t.equal(result, 'ABC static', 'max hunk size < maxLength') 67 | 68 | result = renderTemplate(10, [{ value: function () { 69 | return '' 70 | } }], {}) 71 | t.equal(result, ' ', 'empty value') 72 | 73 | result = renderTemplate(10, [{ value: '12古34', align: 'center', length: '100%' }], {}) 74 | t.equal(result, ' 12古34 ', 'wide chars') 75 | 76 | result = renderTemplate(10, [{ type: 'test', value: 'abc' }], { preTest: '¡', postTest: '!' }) 77 | t.equal(result, '¡abc! ', 'pre/post values') 78 | 79 | result = renderTemplate(10, [{ type: 'test', value: 'abc' }], { preTest: '¡' }) 80 | t.equal(result, '¡abc ', 'pre values') 81 | 82 | result = renderTemplate(10, [{ type: 'test', value: 'abc' }], { postTest: '!' }) 83 | t.equal(result, 'abc! ', 'post values') 84 | 85 | result = renderTemplate(10, [{ value: 'abc' }, { value: '‼‼', length: 0 }, { value: 'def' }]) 86 | t.equal(result, 'abcdef ', 'post values') 87 | 88 | result = renderTemplate(10, [{ value: 'abc', align: 'xyzzy' }]) 89 | t.equal(result, 'abc ', 'unknown aligns are align left') 90 | 91 | t.end() 92 | }) 93 | -------------------------------------------------------------------------------- /test/spin.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const t = require('tap') 3 | const spin = require('../lib/spin') 4 | 5 | t.test('spin', function (t) { 6 | t.plan(2) 7 | const spinner = '123456' 8 | let result 9 | result = spin(spinner, 1) 10 | t.equal(result, '2', 'Spinner 1') 11 | result = spin(spinner, 10) 12 | t.equal(result, '5', 'Spinner 10') 13 | }) 14 | -------------------------------------------------------------------------------- /test/template-item.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const t = require('tap') 3 | const TemplateItem = require('../lib/template-item.js') 4 | 5 | const width = 200 6 | const defaults = { 7 | overallOutputLength: width, 8 | finished: false, 9 | type: null, 10 | value: null, 11 | length: null, 12 | maxLength: null, 13 | minLength: null, 14 | kerning: null, 15 | align: 'left', 16 | padLeft: 0, 17 | padRight: 0, 18 | index: null, 19 | first: null, 20 | last: null, 21 | } 22 | 23 | function got (values) { 24 | return new TemplateItem(values, width) 25 | } 26 | 27 | function expected (obj) { 28 | return Object.assign({}, defaults, obj) 29 | } 30 | 31 | t.test('new', function (t) { 32 | t.strictSame(got('test'), expected({ value: 'test' }), 'str item') 33 | t.strictSame( 34 | got({ value: 'test', length: 3 }), 35 | expected({ value: 'test', length: 3 }), 36 | 'obj item' 37 | ) 38 | t.strictSame(got({ length: '20%' }), expected({ length: 40 }), 'length %') 39 | t.strictSame(got({ maxLength: '10%' }), expected({ maxLength: 20 }), 'length %') 40 | t.strictSame(got({ minLength: '95%' }), expected({ minLength: 190 }), 'length %') 41 | t.end() 42 | }) 43 | 44 | t.test('getBaseLength', function (t) { 45 | var direct = got({ value: 'test', length: 3 }) 46 | t.equal(direct.getBaseLength(), 3, 'directly set') 47 | var intuit = got({ value: 'test' }) 48 | t.equal(intuit.getBaseLength(), 4, 'intuit') 49 | var varmax = got({ value: 'test', maxLength: 4 }) 50 | t.equal(varmax.getBaseLength(), null, 'variable max') 51 | var varmin = got({ value: 'test', minLength: 4 }) 52 | t.equal(varmin.getBaseLength(), null, 'variable min') 53 | t.end() 54 | }) 55 | 56 | t.test('getLength', function (t) { 57 | var direct = got({ value: 'test', length: 3 }) 58 | t.equal(direct.getLength(), 3, 'directly set') 59 | var intuit = got({ value: 'test' }) 60 | t.equal(intuit.getLength(), 4, 'intuit') 61 | var varmax = got({ value: 'test', maxLength: 4 }) 62 | t.equal(varmax.getLength(), null, 'variable max') 63 | var varmin = got({ value: 'test', minLength: 4 }) 64 | t.equal(varmin.getLength(), null, 'variable min') 65 | var pardleft = got({ value: 'test', length: 3, padLeft: 3 }) 66 | t.equal(pardleft.getLength(), 6, 'pad left') 67 | var padright = got({ value: 'test', length: 3, padLeft: 5 }) 68 | t.equal(padright.getLength(), 8, 'pad right') 69 | var padboth = got({ value: 'test', length: 3, padLeft: 5, padRight: 1 }) 70 | t.equal(padboth.getLength(), 9, 'pad both') 71 | t.end() 72 | }) 73 | 74 | t.test('getMaxLength', function (t) { 75 | var nomax = got({ value: 'test' }) 76 | t.equal(nomax.getMaxLength(), null, 'no max length') 77 | var direct = got({ value: 'test', maxLength: 5 }) 78 | t.equal(direct.getMaxLength(), 5, 'max length') 79 | var padleft = got({ value: 'test', maxLength: 5, padLeft: 3 }) 80 | t.equal(padleft.getMaxLength(), 8, 'max length + padLeft') 81 | var padright = got({ value: 'test', maxLength: 5, padRight: 3 }) 82 | t.equal(padright.getMaxLength(), 8, 'max length + padRight') 83 | var padboth = got({ value: 'test', maxLength: 5, padLeft: 2, padRight: 3 }) 84 | t.equal(padboth.getMaxLength(), 10, 'max length + pad both') 85 | t.end() 86 | }) 87 | 88 | t.test('getMinLength', function (t) { 89 | var nomin = got({ value: 'test' }) 90 | t.equal(nomin.getMinLength(), null, 'no min length') 91 | var direct = got({ value: 'test', minLength: 5 }) 92 | t.equal(direct.getMinLength(), 5, 'min length') 93 | var padleft = got({ value: 'test', minLength: 5, padLeft: 3 }) 94 | t.equal(padleft.getMinLength(), 8, 'min length + padLeft') 95 | var padright = got({ value: 'test', minLength: 5, padRight: 3 }) 96 | t.equal(padright.getMinLength(), 8, 'min length + padRight') 97 | var padboth = got({ value: 'test', minLength: 5, padLeft: 2, padRight: 3 }) 98 | t.equal(padboth.getMinLength(), 10, 'min length + pad both') 99 | t.end() 100 | }) 101 | -------------------------------------------------------------------------------- /test/theme-set.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const t = require('tap') 3 | const ThemeSet = require('../lib/theme-set.js') 4 | 5 | const themes = new ThemeSet() 6 | themes.addTheme('fallback', { id: 0 }) 7 | themes.addTheme('test1', { id: 1 }) 8 | themes.addTheme('test2', { id: 2 }) 9 | themes.addTheme('test3', { id: 3 }) 10 | themes.addTheme('test4', { id: 4 }) 11 | themes.addTheme('testz', themes.getTheme('fallback'), { id: 'z' }) 12 | themes.setDefault('fallback') 13 | themes.setDefault({ platform: 'aa', hasUnicode: false, hasColor: false }, 'test1') 14 | themes.setDefault({ platform: 'bb', hasUnicode: true, hasColor: true }, 'test2') 15 | themes.setDefault({ platform: 'ab', hasUnicode: false, hasColor: true }, 'test3') 16 | themes.setDefault({ platform: 'ba', hasUnicode: true, hasColor: false }, 'test4') 17 | 18 | themes.setDefault({ platform: 'zz', hasUnicode: false, hasColor: false }, 'test1') 19 | themes.setDefault({ platform: 'zz', hasUnicode: true, hasColor: true }, 'test2') 20 | themes.setDefault({ platform: 'zz', hasUnicode: false, hasColor: true }, 'test3') 21 | themes.setDefault({ platform: 'zz', hasUnicode: true, hasColor: false }, 'test4') 22 | 23 | t.test('themeset', function (t) { 24 | t.equal(themes().id, 0, 'fallback') 25 | 26 | t.equal(themes({ platform: 'aa' }).id, 1, 'aa ff') 27 | t.equal(themes({ platform: 'aa', hasUnicode: true }).id, 1, 'aa tf') 28 | t.equal(themes({ platform: 'aa', hasColor: true }).id, 1, 'aa ft') 29 | t.equal(themes({ platform: 'aa', hasUnicode: true, hasColor: true }).id, 1, 'aa tt') 30 | t.equal(themes({ platform: 'bb' }).id, 0, 'bb ff') 31 | t.equal(themes({ platform: 'bb', hasUnicode: true }).id, 0, 'bb tf') 32 | t.equal(themes({ platform: 'bb', hasColor: true }).id, 0, 'bb ft') 33 | t.equal(themes({ platform: 'bb', hasUnicode: true, hasColor: true }).id, 2, 'bb tt') 34 | 35 | t.equal(themes({ platform: 'ab' }).id, 0, 'ab ff') 36 | t.equal(themes({ platform: 'ab', hasUnicode: true }).id, 0, 'ab tf') 37 | t.equal(themes({ platform: 'ab', hasColor: true }).id, 3, 'ab ft') 38 | t.equal(themes({ platform: 'ab', hasUnicode: true, hasColor: true }).id, 3, 'ab tt') 39 | 40 | t.equal(themes({ platform: 'ba' }).id, 0, 'ba ff') 41 | t.equal(themes({ platform: 'ba', hasUnicode: true }).id, 4, 'ba tf') 42 | t.equal(themes({ platform: 'ba', hasColor: true }).id, 0, 'ba ft') 43 | t.equal(themes({ platform: 'ba', hasUnicode: true, hasColor: true }).id, 4, 'ba tt') 44 | 45 | t.equal(themes({ platform: 'zz' }).id, 1, 'zz ff') 46 | t.equal(themes({ platform: 'zz', hasUnicode: true }).id, 4, 'zz tf') 47 | t.equal(themes({ platform: 'zz', hasColor: true }).id, 3, 'zz ft') 48 | t.equal(themes({ platform: 'zz', hasUnicode: true, hasColor: true }).id, 2, 'zz tt') 49 | 50 | try { 51 | themes.getTheme('does not exist') 52 | t.fail('missing theme') 53 | } catch (ex) { 54 | t.equal(ex.code, 'EMISSINGTHEME', 'missing theme') 55 | } 56 | 57 | t.equal(themes.getTheme('testz').id, 'z', 'testz') 58 | 59 | var empty = new ThemeSet() 60 | 61 | try { 62 | empty() 63 | t.fail('no themes') 64 | } catch (ex) { 65 | t.equal(ex.code, 'EMISSINGTHEME', 'no themes') 66 | } 67 | 68 | empty.addTheme('exists', { id: 'exists' }) 69 | empty.setDefault({ hasUnicode: true, hasColor: true }, 'exists') 70 | try { 71 | empty() 72 | t.fail('no fallback') 73 | } catch (ex) { 74 | t.equal(ex.code, 'EMISSINGTHEME', 'no fallback') 75 | } 76 | t.end() 77 | }) 78 | 79 | t.test('add-to-all', function (t) { 80 | themes.addToAllThemes({ 81 | xyz: 17, 82 | }) 83 | t.equal(themes.getTheme('test1').xyz, 17, 'existing themes updated') 84 | var newTheme = themes.newTheme({ id: 99 }) 85 | t.equal(newTheme.id, 99, 'new theme initialized') 86 | t.equal(newTheme.xyz, 17, 'new theme got extension') 87 | t.end() 88 | }) 89 | -------------------------------------------------------------------------------- /test/themes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const t = require('tap') 3 | const themes = require('../lib/themes.js') 4 | 5 | t.test('selector', function (t) { 6 | t.equal( 7 | themes({ hasUnicode: false, hasColor: false, platform: 'unknown' }), 8 | themes.getTheme('ASCII'), 9 | 'fallback' 10 | ) 11 | t.equal( 12 | themes({ hasUnicode: false, hasColor: false, platform: 'darwin' }), 13 | themes.getTheme('ASCII'), 14 | 'ff darwin' 15 | ) 16 | t.equal( 17 | themes({ hasUnicode: true, hasColor: false, platform: 'darwin' }), 18 | themes.getTheme('brailleSpinner'), 19 | 'tf drawin' 20 | ) 21 | t.equal( 22 | themes({ hasUnicode: false, hasColor: true, platform: 'darwin' }), 23 | themes.getTheme('colorASCII'), 24 | 'ft darwin' 25 | ) 26 | t.equal( 27 | themes({ hasUnicode: true, hasColor: true, platform: 'darwin' }), 28 | themes.getTheme('colorBrailleSpinner'), 29 | 'ft darwin' 30 | ) 31 | t.end() 32 | }) 33 | -------------------------------------------------------------------------------- /test/wide-truncate.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const t = require('tap') 3 | const wideTruncate = require('../lib/wide-truncate.js') 4 | 5 | t.test('wideTruncate', function (t) { 6 | let result 7 | 8 | result = wideTruncate('abc', 6) 9 | t.equal(result, 'abc', 'narrow, no truncation') 10 | result = wideTruncate('古古古', 6) 11 | t.equal(result, '古古古', 'wide, no truncation') 12 | result = wideTruncate('abc', 2) 13 | t.equal(result, 'ab', 'narrow, truncation') 14 | result = wideTruncate('古古古', 2) 15 | t.equal(result, '古', 'wide, truncation') 16 | result = wideTruncate('古古', 3) 17 | t.equal(result, '古', 'wide, truncation, partial') 18 | result = wideTruncate('古', 1) 19 | t.equal(result, '', 'wide, truncation, no chars fit') 20 | result = wideTruncate('abc', 0) 21 | t.equal(result, '', 'zero truncation is empty') 22 | result = wideTruncate('', 10) 23 | t.equal(result, '', 'empty string') 24 | 25 | result = wideTruncate('abc古古古def', 12) 26 | t.equal(result, 'abc古古古def', 'mixed nwn, no truncation') 27 | result = wideTruncate('abcdef古古古', 12) 28 | t.equal(result, 'abcdef古古古', 'mixed nw, no truncation') 29 | result = wideTruncate('古古古abcdef', 12) 30 | t.equal(result, '古古古abcdef', 'mixed wn, no truncation') 31 | result = wideTruncate('古古abcdef古', 12) 32 | t.equal(result, '古古abcdef古', 'mixed wnw, no truncation') 33 | 34 | result = wideTruncate('abc古古古def', 6) 35 | t.equal(result, 'abc古', 'mixed nwn, truncation') 36 | result = wideTruncate('abcdef古古古', 6) 37 | t.equal(result, 'abcdef', 'mixed nw, truncation') 38 | result = wideTruncate('古古古abcdef', 6) 39 | t.equal(result, '古古古', 'mixed wn, truncation') 40 | result = wideTruncate('古古abcdef古', 6) 41 | t.equal(result, '古古ab', 'mixed wnw, truncation') 42 | result = wideTruncate('abc\x1b[0mdef', 6) 43 | t.equal(result, 'abc\x1b[0mdef', 'ansi codes are zero width') 44 | result = wideTruncate('abc\x1b[0mdef', 4) 45 | t.equal(result, 'abc\x1b[0md', 'ansi codes are zero width, clip text') 46 | 47 | t.end() 48 | }) 49 | --------------------------------------------------------------------------------