├── .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 |
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 |
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 |
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 | 
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 |
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 |
--------------------------------------------------------------------------------