• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

peterjwest / unlinted / 0d7d72eb-711b-4c7a-a710-2ae23bfff973

17 Aug 2023 12:35PM UTC coverage: 6.254% (-0.4%) from 6.7%
0d7d72eb-711b-4c7a-a710-2ae23bfff973

push

circleci

peterjwest
Improve readme description

2 of 18 branches covered (11.11%)

Branch coverage included in aggregate %.

93 of 1501 relevant lines covered (6.2%)

0.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/ProgressManager.ts
1
import chalk from 'chalk';
×
2
import lodash from 'lodash';
×
3

×
4
import { createEnum } from './util';
×
5

×
6
type SectionStatus = keyof typeof SECTION_STATUSES;
×
7

×
8
interface Section {
×
9
  name: string;
×
10
  status: SectionStatus;
×
11
}
×
12

×
13
interface ProgressBar {
×
14
  message: string;
×
15
  succeeded: number;
×
16
  failed: number;
×
17
  total: number;
×
18
}
×
19

×
20
const SECTION_STATUSES = createEnum([
×
21
  'IN_PROGRESS',
×
22
  'SUCCESS',
×
23
  'FAILURE',
×
24
]);
×
25

×
26
const SECTION_STATUS_ICONS = {
×
27
  IN_PROGRESS: chalk.cyan('>'),
×
28
  SUCCESS: chalk.green('✓'),
×
29
  FAILURE: chalk.red('×'),
×
30
} as const satisfies { [Property in SectionStatus]: string };
×
31

×
32
/** Frame delay to render progress in milliseconds */
×
33
const FRAME_INTERVAL = 1000 / 20; // 50 ms = 1000 ms / 20 fps
×
34

×
35
const SAVE_CURSOR = '\u001b7';
×
36
const RESTORE_CURSOR = '\u001b8';
×
37
const MOVE_CURSOR = '\u001b';
×
38
const CLEAR_LINE = '\u001b[2K';
×
39

×
40
/** Moves the cursor up a number of lines in the terminal */
×
41
function moveCursorUp(lines: number) {
×
42
  return MOVE_CURSOR + '[' + lines + 'A';
×
43
}
×
44

×
45
/** Rewrites a line in the terminal */
×
46
function rewriteLine(stream: NodeJS.WriteStream, line: number, text: string) {
×
47
  stream.write(SAVE_CURSOR);
×
48
  stream.write(moveCursorUp(line));
×
49
  stream.write(CLEAR_LINE);
×
50
  stream.write(text);
×
51
  stream.write(RESTORE_CURSOR);
×
52
}
×
53

×
54
/** Returns a section with its current icon */
×
55
function outputSection(section: Section) {
×
56
  return `${SECTION_STATUS_ICONS[section.status]} ${chalk.grey(section.name)}\n`;
×
57
}
×
58

×
59
/** Returns a progress bar */
×
60
function outputProgressBar({ succeeded, failed, total, message }: ProgressBar, width = 40) {
×
61
  const successWidth = Math.floor(width * (succeeded / total));
×
62
  const errorWidth = Math.floor(width * (failed / total));
×
63
  // Ensure any non-zero number of errors are visible */
×
64
  const clampedErrorWidth = failed > 0 ? Math.max(errorWidth, 1) : 0;
×
65
  return [
×
66
    chalk.green('▰'.repeat(successWidth)),
×
67
    chalk.grey('▱'.repeat(width - successWidth - errorWidth)),
×
68
    chalk.red('▰'.repeat(clampedErrorWidth)),
×
69
    ` ${message}\n`,
×
70
  ].join('');
×
71
}
×
72

×
73
/** Manages named entries (sections) and their status in a terminal stream */
×
74
export default class ProgressManager {
×
75
  interactive: boolean;
×
76
  stream: NodeJS.WriteStream;
×
77
  sections: Section[] = [];
×
78
  progress?: ProgressBar;
×
79

×
80
  constructor(stream: NodeJS.WriteStream) {
×
81
    this.stream = stream;
×
82
    this.interactive = stream.isTTY;
×
83
  }
×
84

×
85
  /** Manages terminal output for an async task */
×
86
  static async manage<Result>(
×
87
    stream: NodeJS.WriteStream,
×
88
    task: (progress: ProgressManager) => Promise<Result>,
×
89
  ): Promise<Result> {
×
90

×
91
    const progress = new ProgressManager(process.stdout);
×
92
    try {
×
93
      return await task(progress);
×
94
    } finally {
×
95
      progress.end();
×
96
    }
×
97
  }
×
98

×
99
  /** Redraws a section */
×
100
  redrawSection(section: Section) {
×
101
    rewriteLine(this.stream, 1 + (this.progress ? 1 : 0), outputSection(section));
×
102
  }
×
103

×
104
  /** Redraws the progress bar, throttled to the frame interval */
×
105
  redrawProgressBar = lodash.throttle((progress: ProgressBar) => {
×
106
    rewriteLine(this.stream, 1, outputProgressBar(progress));
×
107
  }, FRAME_INTERVAL);
×
108

×
109
  /** Adds an in-progress section to be displayed with an optional progress bar */
×
110
  addSection(name: string, total?: number) {
×
111
    const index = this.sections.length - 1;
×
112
    if (index >= 0) {
×
113
      this.sectionResult(true);
×
114
      this.progressBarDone();
×
115
    }
×
116
    const section = { name, status: SECTION_STATUSES.IN_PROGRESS };
×
117
    this.sections.push(section);
×
118
    this.stream.write(outputSection(section));
×
119

×
120
    if (this.interactive && total !== undefined) {
×
121
      this.progress = { message: '', succeeded: 0, failed: 0, total };
×
122
      this.stream.write(outputProgressBar(this.progress));
×
123
    }
×
124

×
125
    return this.sections.length - 1;
×
126
  }
×
127

×
128
  /** Increments the successes or failures of the progress bar by 1 */
×
129
  incrementProgress(success: boolean) {
×
130
    if (!this.interactive) return;
×
131
    if (!this.progress) throw new Error('No progress bar to update');
×
132
    this.progress[success ? 'succeeded' : 'failed']++;
×
133
    this.redrawProgressBar(this.progress);
×
134
  }
×
135

×
136
  /** Updates the progress bar message */
×
137
  progressBarMessage(message: string) {
×
138
    if (!this.interactive) return;
×
139
    if (!this.progress) throw new Error('No progress bar to update');
×
140
    this.progress.message = message;
×
141
    this.redrawProgressBar(this.progress);
×
142
  }
×
143

×
144
  /** Marks the last section as succeeded or failed, if not already complete */
×
145
  sectionResult(success: boolean) {
×
146
    const section = this.sections[this.sections.length - 1];
×
147
    if (!section) return;
×
148
    if (section.status === SECTION_STATUSES.IN_PROGRESS) {
×
149
      section.status = success ? SECTION_STATUSES.SUCCESS : SECTION_STATUSES.FAILURE;
×
150
      if (this.interactive) this.redrawSection(section);
×
151
    }
×
152
  }
×
153

×
154
  /** Removes the progress bar */
×
155
  progressBarDone() {
×
156
    if (this.progress) {
×
157
      this.progress = undefined;
×
158
      this.stream.write(moveCursorUp(1));
×
159
      this.stream.write(CLEAR_LINE);
×
160
    }
×
161
  }
×
162

×
163
  /** Marks the last section as failed, if not already complete */
×
164
  sectionFailed() {
×
165
    this.sectionResult(false);
×
166
  }
×
167

×
168
  /** Ends the final section, removes the progress bar */
×
169
  end() {
×
170
    this.redrawProgressBar.cancel();
×
171
    this.sectionResult(true);
×
172
    this.progressBarDone();
×
173
  }
×
174
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc