Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 36 additions & 34 deletions lib/utils/display.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,11 @@ class Display {
timing,
unicode,
}) {
// get createSupportsColor from chalk directly if this lands
// https://github.com/chalk/chalk/pull/600
const [{ Chalk }, { createSupportsColor }] = await Promise.all([
import('chalk'),
import('supports-color'),
])
// we get the chalk level based on a null stream meaning chalk will only use what it knows about the environment to get color support since we already determined in our definitions that we want to show colors.
// We get the chalk level based on a null stream, meaning chalk will only use what it knows about the environment to get color support since we already determined in our definitions that we want to show colors.
const level = Math.max(createSupportsColor(null).level, 1)
this.#noColorChalk = new Chalk({ level: 0 })
this.#stdoutColor = stdoutColor
Expand Down Expand Up @@ -307,14 +305,14 @@ class Display {
if (this.#outputState.buffering) {
this.#outputState.buffer.push([level, meta, ...args])
} else {
// HACK: Check if the argument looks like a run-script banner. This can be replaced with proc-log.META in @npmcli/run-script
// XXX: Check if the argument looks like a run-script banner. This should be replaced with proc-log.META in @npmcli/run-script
if (typeof args[0] === 'string' && args[0].startsWith('\n> ') && args[0].endsWith('\n')) {
if (this.#silent || ['exec', 'explore'].includes(this.#command)) {
// Silent mode and some specific commands always hide run script banners
break
} else if (this.#json) {
// In json mode, change output to stderr since we don't want to break json parsing on stdout if the user is piping to jq or something.
// XXX: in a future (breaking?) change it might make sense for run-script to always output these banners with proc-log.output.error if we think they align closer with "logging" instead of "output"
// XXX: in a future (breaking?) change it might make sense for run-script to always output these banners with proc-log.output.error if we think they align closer with "logging" instead of "output".
level = output.KEYS.error
}
}
Expand All @@ -339,12 +337,12 @@ class Display {
break

case input.KEYS.read: {
// The convention when calling input.read is to pass in a single fn that returns the promise to await. resolve and reject are provided by proc-log
// The convention when calling input.read is to pass in a single fn that returns the promise to await. resolve and reject are provided by proc-log.
const [res, rej, p] = args
return input.start(() => p()
.then(res)
.catch(rej)
// Any call to procLog.input.read will render a prompt to the user, so we always add a single newline of output to stdout to move the cursor to the next line
// Any call to procLog.input.read will render a prompt to the user, so we always add a single newline of output to stdout to move the cursor to the next line.
.finally(() => output.standard('')))
}
}
Expand Down Expand Up @@ -419,16 +417,17 @@ class Progress {
static dots = { duration: 80, frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] }
static lines = { duration: 130, frames: ['-', '\\', '|', '/'] }

#stream
#spinner
#enabled = false

#frameIndex = 0
#lastUpdate = 0
#interval
#lastUpdate = 0
#spinner
#stream
// Initial timeout to wait to start rendering
#timeout
#rendered = false

// We are rendering is enabled option is set and we are not waiting for the render timeout
// We are rendering if enabled option is set and we are not waiting for the render timeout
get #rendering () {
return this.#enabled && !this.#timeout
}
Expand All @@ -445,8 +444,13 @@ class Progress {
load ({ enabled, unicode }) {
this.#enabled = enabled
this.#spinner = unicode ? Progress.dots : Progress.lines
// Dont render the spinner for short durations
this.#render(200)
// Wait 200 ms so we don't render the spinner for short durations
this.#timeout = setTimeout(() => {
this.#timeout = null
this.#render()
}, 200)
// Make sure this timeout does not keep the process open
this.#timeout.unref()
}

off () {
Expand All @@ -463,7 +467,7 @@ class Progress {
}

resume () {
this.#render()
this.#render(true)
}

// If we are currently rendering the spinner we clear it before writing our line and then re-render the spinner after.
Expand All @@ -478,45 +482,43 @@ class Progress {
}
}

#render (ms) {
if (ms) {
this.#timeout = setTimeout(() => {
this.#timeout = null
this.#renderSpinner()
}, ms)
// Make sure this timeout does not keep the process open
this.#timeout.unref()
} else {
this.#renderSpinner()
}
}

#renderSpinner () {
#render (resuming) {
if (!this.#rendering) {
return
}
// We always attempt to render immediately but we only request to move to the next frame if it has been longer than our spinner frame duration since our last update
this.#renderFrame(Date.now() - this.#lastUpdate >= this.#spinner.duration)
clearInterval(this.#interval)
this.#interval = setInterval(() => this.#renderFrame(true), this.#spinner.duration)
this.#renderFrame(Date.now() - this.#lastUpdate >= this.#spinner.duration, resuming)
if (!this.#interval) {
this.#interval = setInterval(() => this.#renderFrame(true), this.#spinner.duration)
// Make sure this timeout does not keep the process open
this.#interval.unref()
}
this.#interval.refresh()
}

#renderFrame (next) {
#renderFrame (next, resuming) {
if (next) {
this.#lastUpdate = Date.now()
this.#frameIndex++
if (this.#frameIndex >= this.#spinner.frames.length) {
this.#frameIndex = 0
}
}
this.#clearSpinner()
if (!resuming) {
this.#clearSpinner()
}
this.#stream.write(this.#spinner.frames[this.#frameIndex])
this.#rendered = true
}

#clearSpinner () {
if (!this.#rendered) {
return
}
// Move to the start of the line and clear the rest of the line
this.#stream.cursorTo(0)
this.#stream.clearLine(1)
this.#rendered = false
}
}

Expand Down