@oclif/multi-stage-output
Advanced tools
Comparing version 0.4.0 to 0.4.1-dev.0
import React from 'react'; | ||
export declare function Divider({ dividerChar, dividerColor, padding, textColor, textPadding: titlePadding, title, width, }: { | ||
export declare function Divider({ dividerChar, dividerColor, padding, terminalWidth, textColor, textPadding, title, width, }: { | ||
readonly title?: string; | ||
@@ -10,2 +10,3 @@ readonly width?: number | 'full'; | ||
readonly dividerColor?: string; | ||
readonly terminalWidth?: number; | ||
}): React.ReactNode; |
@@ -6,7 +6,13 @@ import { Box, Text } from 'ink'; | ||
const PAD = ' '; | ||
export function Divider({ dividerChar = '─', dividerColor = 'dim', padding = 1, textColor, textPadding: titlePadding = 1, title = '', width = 50, }) { | ||
const titleString = title ? `${PAD.repeat(titlePadding) + title + PAD.repeat(titlePadding)}` : ''; | ||
export function Divider({ dividerChar = '─', dividerColor = 'dim', padding = 1, terminalWidth = process.stdout.columns ?? 80, textColor, textPadding = 1, title = '', width = 50, }) { | ||
const titleString = title ? `${PAD.repeat(textPadding) + title + PAD.repeat(textPadding)}` : ''; | ||
const titleWidth = titleString.length; | ||
const terminalWidth = process.stdout.columns ?? 80; | ||
const widthToUse = width === 'full' ? terminalWidth - titlePadding : width > terminalWidth ? terminalWidth : width; | ||
const widthToUse = width === 'full' | ||
? // if the width is `full`, use the terminal width minus the padding and title padding | ||
terminalWidth - textPadding - padding | ||
: // otherwise, if the provided width is greater than the terminal width, use the terminal width minus the padding and title paddding | ||
width > terminalWidth | ||
? terminalWidth - textPadding - padding | ||
: // otherwise, use the provided width | ||
width; | ||
const dividerWidth = getSideDividerWidth(widthToUse, titleWidth); | ||
@@ -13,0 +19,0 @@ const numberOfCharsPerSide = getNumberOfCharsPerWidth(dividerChar, dividerWidth); |
import { type SpinnerName } from 'cli-spinners'; | ||
import React from 'react'; | ||
import { IconProps } from './icon.js'; | ||
import { RequiredDesign } from '../design.js'; | ||
type UseSpinnerProps = { | ||
@@ -22,5 +22,5 @@ /** | ||
export declare function Spinner({ isBold, label, labelPosition, type }: SpinnerProps): React.ReactElement; | ||
export declare function SpinnerOrError({ error, failedIcon, labelPosition, ...props }: SpinnerProps & { | ||
export declare function SpinnerOrError({ design, error, labelPosition, ...props }: SpinnerProps & { | ||
readonly error?: Error; | ||
readonly failedIcon: IconProps; | ||
readonly design: RequiredDesign; | ||
}): React.ReactElement; | ||
@@ -30,4 +30,4 @@ export declare function SpinnerOrErrorOrChildren({ children, error, ...props }: SpinnerProps & { | ||
readonly error?: Error; | ||
readonly failedIcon: IconProps; | ||
readonly design: RequiredDesign; | ||
}): React.ReactElement; | ||
export {}; |
@@ -34,3 +34,3 @@ import spinners from 'cli-spinners'; | ||
} | ||
export function SpinnerOrError({ error, failedIcon, labelPosition = 'right', ...props }) { | ||
export function SpinnerOrError({ design, error, labelPosition = 'right', ...props }) { | ||
if (error) { | ||
@@ -41,3 +41,3 @@ return (React.createElement(Box, null, | ||
" "), | ||
React.createElement(Icon, { icon: failedIcon }), | ||
React.createElement(Icon, { icon: design.icons.failed }), | ||
props.label && labelPosition === 'right' && React.createElement(Text, null, | ||
@@ -44,0 +44,0 @@ " ", |
@@ -27,2 +27,6 @@ import React from 'react'; | ||
bold?: boolean; | ||
/** | ||
* Set to `true` to prevent this key-value pair or message from being collapsed when the window is too short. Defaults to false. | ||
*/ | ||
neverCollapse?: boolean; | ||
}; | ||
@@ -52,16 +56,37 @@ export type KeyValuePair<T extends Record<string, unknown>> = Info<T> & { | ||
readonly type: 'dynamic-key-value' | 'static-key-value' | 'message'; | ||
readonly neverCollapse?: boolean; | ||
}; | ||
export type StagesProps = { | ||
readonly compactionLevel?: number; | ||
readonly design?: RequiredDesign; | ||
readonly error?: Error | undefined; | ||
readonly hasElapsedTime?: boolean; | ||
readonly hasStageTime?: boolean; | ||
readonly postStagesBlock?: FormattedKeyValue[]; | ||
readonly preStagesBlock?: FormattedKeyValue[]; | ||
readonly stageSpecificBlock?: FormattedKeyValue[]; | ||
readonly stageTracker: StageTracker; | ||
readonly timerUnit?: 'ms' | 's'; | ||
readonly title?: string; | ||
readonly hasElapsedTime?: boolean; | ||
readonly hasStageTime?: boolean; | ||
readonly timerUnit?: 'ms' | 's'; | ||
readonly stageTracker: StageTracker; | ||
}; | ||
export declare function Stages({ design, error, hasElapsedTime, hasStageTime, postStagesBlock, preStagesBlock, stageSpecificBlock, stageTracker, timerUnit, title, }: StagesProps): React.ReactNode; | ||
/** | ||
* Determine the level of compaction required to render the stages component within the terminal height. | ||
* | ||
* Compaction levels: | ||
* 0 - hide nothing | ||
* 1 - only show one stage at a time, with stage specific info nested under the stage | ||
* 2 - hide the elapsed time | ||
* 3 - hide the title | ||
* 4 - hide the pre-stages block | ||
* 5 - hide the post-stages block | ||
* 6 - put the stage specific info directly next to the stage | ||
* 7 - hide the stage-specific block | ||
* 8 - reduce the padding between boxes | ||
* @returns the compaction level based on the number of lines that will be displayed | ||
*/ | ||
export declare function determineCompactionLevel({ design, hasElapsedTime, hasStageTime, postStagesBlock, preStagesBlock, stageSpecificBlock, stageTracker, title, }: StagesProps, rows: number, columns: number): { | ||
compactionLevel: number; | ||
totalHeight: number; | ||
}; | ||
export declare function Stages({ compactionLevel, design, error, hasElapsedTime, hasStageTime, postStagesBlock, preStagesBlock, stageSpecificBlock, stageTracker, timerUnit, title, }: StagesProps): React.ReactNode; | ||
export {}; |
import { capitalCase } from 'change-case'; | ||
import { Box, Text } from 'ink'; | ||
import { Box, Text, useStdout } from 'ink'; | ||
import React from 'react'; | ||
@@ -36,3 +36,3 @@ import { constructDesignParams } from '../design.js'; | ||
React.createElement(Icon, { icon: design.icons.info }), | ||
React.createElement(SpinnerOrErrorOrChildren, { error: error, label: `${kv.label}:`, labelPosition: "left", type: design.spinners.info, failedIcon: design.icons.failed }, kv.value && (React.createElement(Text, { bold: kv.isBold, color: kv.color }, kv.value))))); | ||
React.createElement(SpinnerOrErrorOrChildren, { error: error, label: `${kv.label}:`, labelPosition: "left", type: design.spinners.info, design: design }, kv.value && (React.createElement(Text, { bold: kv.isBold, color: kv.color }, kv.value))))); | ||
} | ||
@@ -54,3 +54,3 @@ if (kv.type === 'static-key-value') { | ||
if (kv.type === 'dynamic-key-value') { | ||
return (React.createElement(SpinnerOrErrorOrChildren, { key: key, error: error, label: `${kv.label}:`, labelPosition: "left", type: design.spinners.info, failedIcon: design.icons.failed }, kv.value && (React.createElement(Text, { bold: kv.isBold, color: kv.color }, kv.value)))); | ||
return (React.createElement(SpinnerOrErrorOrChildren, { key: key, error: error, label: `${kv.label}:`, labelPosition: "left", type: design.spinners.info, design: design }, kv.value && (React.createElement(Text, { bold: kv.isBold, color: kv.color }, kv.value)))); | ||
} | ||
@@ -63,27 +63,246 @@ if (kv.type === 'static-key-value') { | ||
} | ||
export function Stages({ design = constructDesignParams(), error, hasElapsedTime = true, hasStageTime = true, postStagesBlock, preStagesBlock, stageSpecificBlock, stageTracker, timerUnit = 'ms', title, }) { | ||
return (React.createElement(Box, { flexDirection: "column", paddingTop: 1, paddingBottom: 1 }, | ||
title && React.createElement(Divider, { title: title, ...design.title }), | ||
preStagesBlock && preStagesBlock.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 1, paddingTop: 1 }, | ||
React.createElement(Infos, { design: design, error: error, keyValuePairs: preStagesBlock }))), | ||
React.createElement(Box, { flexDirection: "column", marginLeft: 1, paddingTop: 1 }, [...stageTracker.entries()].map(([stage, status]) => (React.createElement(Box, { key: stage, flexDirection: "column" }, | ||
React.createElement(Box, null, | ||
(status === 'current' || status === 'failed') && (React.createElement(SpinnerOrError, { error: error, label: capitalCase(stage), type: design.spinners.stage, failedIcon: design.icons.failed })), | ||
status === 'skipped' && (React.createElement(Icon, { icon: design.icons.skipped }, | ||
React.createElement(Text, { color: "dim" }, | ||
capitalCase(stage), | ||
" - Skipped"))), | ||
status === 'completed' && (React.createElement(Icon, { icon: design.icons.completed }, | ||
React.createElement(Text, null, capitalCase(stage)))), | ||
status === 'pending' && (React.createElement(Icon, { icon: design.icons.pending }, | ||
React.createElement(Text, null, capitalCase(stage)))), | ||
status !== 'pending' && status !== 'skipped' && hasStageTime && (React.createElement(Box, null, | ||
React.createElement(Text, null, " "), | ||
React.createElement(Timer, { color: "dim", isStopped: status === 'completed', unit: timerUnit })))), | ||
stageSpecificBlock && stageSpecificBlock.length > 0 && status !== 'pending' && status !== 'skipped' && (React.createElement(StageInfos, { design: design, error: error, keyValuePairs: stageSpecificBlock, stage: stage })))))), | ||
postStagesBlock && postStagesBlock.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 1, paddingTop: 1 }, | ||
React.createElement(Infos, { design: design, error: error, keyValuePairs: postStagesBlock }))), | ||
hasElapsedTime && (React.createElement(Box, { marginLeft: 1 }, | ||
function CompactStage({ design, direction = 'row', error, stage, stageSpecificBlock, stageTracker, status, }) { | ||
if (status !== 'current') | ||
return false; | ||
return (React.createElement(Box, { flexDirection: direction }, | ||
React.createElement(SpinnerOrError, { error: error, label: `[${stageTracker.indexOf(stage) + 1}/${stageTracker.size}] ${capitalCase(stage)}`, type: design.spinners.stage, design: design }), | ||
stageSpecificBlock && stageSpecificBlock.length > 0 && (React.createElement(Box, { flexDirection: "column" }, | ||
React.createElement(StageInfos, { design: design, error: error, keyValuePairs: stageSpecificBlock, stage: stage }))))); | ||
} | ||
function Stage({ design, error, stage, status, }) { | ||
return (React.createElement(Box, null, | ||
(status === 'current' || status === 'failed') && (React.createElement(SpinnerOrError, { error: error, label: capitalCase(stage), type: design.spinners.stage, design: design })), | ||
status === 'skipped' && (React.createElement(Icon, { icon: design.icons.skipped }, | ||
React.createElement(Text, { color: "dim" }, | ||
capitalCase(stage), | ||
" - Skipped"))), | ||
status !== 'skipped' && status !== 'failed' && status !== 'current' && (React.createElement(Icon, { icon: design.icons[status] }, | ||
React.createElement(Text, null, capitalCase(stage)))))); | ||
} | ||
function StageEntries({ compactionLevel, design, error, hasStageTime, stageSpecificBlock, stageTracker, timerUnit, }) { | ||
return (React.createElement(React.Fragment, null, [...stageTracker.entries()].map(([stage, status]) => (React.createElement(Box, { key: stage, flexDirection: "column" }, | ||
React.createElement(Box, null, | ||
compactionLevel === 0 ? (React.createElement(Stage, { stage: stage, status: status, design: design, error: error })) : ( | ||
// Render the stage name, spinner, and stage specific info | ||
React.createElement(CompactStage, { stage: stage, status: status, design: design, error: error, stageSpecificBlock: stageSpecificBlock, stageTracker: stageTracker, direction: compactionLevel >= 6 ? 'row' : 'column' })), | ||
status !== 'pending' && status !== 'skipped' && hasStageTime && (React.createElement(Box, { display: compactionLevel === 0 ? 'flex' : status === 'current' ? 'flex' : 'none' }, | ||
React.createElement(Text, null, " "), | ||
React.createElement(Timer, { color: "dim", isStopped: status === 'completed', unit: timerUnit })))), | ||
compactionLevel === 0 && | ||
stageSpecificBlock && | ||
stageSpecificBlock.length > 0 && | ||
status !== 'pending' && | ||
status !== 'skipped' && (React.createElement(StageInfos, { design: design, error: error, keyValuePairs: stageSpecificBlock, stage: stage }))))))); | ||
} | ||
function filterInfos(infos, compactionLevel, cutOff) { | ||
return infos.filter((info) => { | ||
// return true to keep the info | ||
if (compactionLevel < cutOff || info.neverCollapse) { | ||
return true; | ||
} | ||
return false; | ||
}); | ||
} | ||
/** | ||
* Determine the level of compaction required to render the stages component within the terminal height. | ||
* | ||
* Compaction levels: | ||
* 0 - hide nothing | ||
* 1 - only show one stage at a time, with stage specific info nested under the stage | ||
* 2 - hide the elapsed time | ||
* 3 - hide the title | ||
* 4 - hide the pre-stages block | ||
* 5 - hide the post-stages block | ||
* 6 - put the stage specific info directly next to the stage | ||
* 7 - hide the stage-specific block | ||
* 8 - reduce the padding between boxes | ||
* @returns the compaction level based on the number of lines that will be displayed | ||
*/ | ||
export function determineCompactionLevel({ design = constructDesignParams(), hasElapsedTime, hasStageTime, postStagesBlock, preStagesBlock, stageSpecificBlock, stageTracker, title, }, rows, columns) { | ||
const calculateHeightOfBlock = (block) => { | ||
if (!block) | ||
return 0; | ||
return block.reduce((acc, info) => { | ||
if (info.type === 'message') { | ||
if (!info.value) | ||
return acc; | ||
if (info.value.length > columns) { | ||
// if the message is longer than the terminal width, add the number of lines | ||
return acc + Math.ceil(info.value.length / columns); | ||
} | ||
// if the message is multiline, add the number of lines | ||
return acc + info.value.split('\n').length; | ||
} | ||
const { label = '', value } = info; | ||
// if there's no value we still add 1 for the label | ||
if (!value) | ||
return acc + 1; | ||
if (label.length + Number(': '.length) + value.length > columns) { | ||
// if the value is longer than the terminal width, add the number of lines | ||
return acc + Math.ceil(value.length / columns); | ||
} | ||
return acc + value.split('\n').length; | ||
}, 0); | ||
}; | ||
const calculateHeightOfStage = (stage) => { | ||
const status = stageTracker.get(stage) ?? 'pending'; | ||
const skipped = status === 'skipped' ? ' - Skipped' : ''; | ||
// We don't have access to the exact stage time, so we're taking a conservative estimate of | ||
// 10 characters + 1 character for the space between the stage and timer, | ||
// examples: 999ms (5), 59.99s (6), 59m 59.99s (10), 23h 59m (7) | ||
const stageTimeLength = hasStageTime ? 11 : 0; | ||
if ( | ||
// 1 for the left margin | ||
1 + | ||
design.icons[status].paddingLeft + | ||
design.icons[status].figure.length + | ||
design.icons[status].paddingRight + | ||
stage.length + | ||
skipped.length + | ||
stageTimeLength > | ||
columns) { | ||
return Math.ceil(stage.length / columns); | ||
} | ||
return 1; | ||
}; | ||
const calculateWidthOfCompactStage = (stage) => { | ||
const status = stageTracker.get(stage) ?? 'current'; | ||
// We don't have access to the exact stage time, so we're taking a conservative estimate of | ||
// 7 characters + 1 character for the space between the stage and timer, | ||
// examples: 999ms (5), 59s (3), 59m 59s (7), 23h 59m (7) | ||
const stageTimeLength = hasStageTime ? 8 : 0; | ||
const firstStageSpecificBlock = stageSpecificBlock?.find((block) => block.stage === stage); | ||
const firstStageSpecificBlockLength = firstStageSpecificBlock?.type === 'message' | ||
? (firstStageSpecificBlock?.value?.length ?? 0) | ||
: (firstStageSpecificBlock?.label?.length ?? 0) + (firstStageSpecificBlock?.value?.length ?? 0) + 2; | ||
const width = | ||
// 1 for the left margin | ||
1 + | ||
design.icons[status].paddingLeft + | ||
design.icons[status].figure.length + | ||
design.icons[status].paddingRight + | ||
`[${stageTracker.indexOf(stage) + 1}/${stageTracker.size}] ${stage}`.length + | ||
stageTimeLength + | ||
firstStageSpecificBlockLength; | ||
return width; | ||
}; | ||
const stagesHeight = [...stageTracker.values()].reduce((acc, stage) => acc + calculateHeightOfStage(stage), 0); | ||
const preStagesBlockHeight = calculateHeightOfBlock(preStagesBlock); | ||
const postStagesBlockHeight = calculateHeightOfBlock(postStagesBlock); | ||
const stageSpecificBlockHeight = calculateHeightOfBlock(stageSpecificBlock); | ||
// 3 at minimum because: 1 for marginTop on entire component, 1 for marginBottom on entire component, 1 for paddingBottom on StageEntries | ||
const paddings = 3 + (preStagesBlock ? 1 : 0) + (postStagesBlock ? 1 : 0) + (title ? 1 : 0); | ||
const totalHeight = stagesHeight + | ||
preStagesBlockHeight + | ||
postStagesBlockHeight + | ||
stageSpecificBlockHeight + | ||
(title ? 1 : 0) + | ||
(hasElapsedTime ? 1 : 0) + | ||
paddings + | ||
// add one for good measure - iTerm2 will flicker on every render if the height is exactly the same as the terminal height so it's better to be safe | ||
1; | ||
let cLevel = 0; | ||
const levels = [ | ||
// 1: only show one stage at a time, with stage specific info nested under the stage | ||
(remainingHeight) => remainingHeight - stagesHeight + 1, | ||
// 2: hide the elapsed time | ||
(remainingHeight) => remainingHeight - 1, | ||
// 3: hide the title (subtract 1 for title and 1 for paddingBottom) | ||
(remainingHeight) => remainingHeight - 2, | ||
// 4: hide the pre-stages block (subtract 1 for paddingBottom) | ||
(remainingHeight) => remainingHeight - preStagesBlockHeight - 1, | ||
// 5: hide the post-stages block | ||
(remainingHeight) => remainingHeight - postStagesBlockHeight, | ||
// 6: put the stage specific info directly next to the stage | ||
(remainingHeight) => remainingHeight - stageSpecificBlockHeight, | ||
// 7: hide the stage-specific block | ||
(remainingHeight) => remainingHeight - stageSpecificBlockHeight, | ||
// 8: reduce the padding between boxes | ||
(remainingHeight) => remainingHeight - 1, | ||
]; | ||
let remainingHeight = totalHeight; | ||
while (cLevel < 8 && remainingHeight >= rows) { | ||
remainingHeight = levels[cLevel](remainingHeight); | ||
cLevel++; | ||
} | ||
// It's possible that the collapsed stage might extend beyond the terminal width. | ||
// If so, we need to bump the compaction level up to 7 so that the stage specific info is hidden | ||
if (cLevel === 6 && stageTracker.current && calculateWidthOfCompactStage(stageTracker.current) >= columns) { | ||
cLevel = 7; | ||
} | ||
return { | ||
compactionLevel: cLevel, | ||
totalHeight, | ||
}; | ||
} | ||
export function Stages({ compactionLevel, design = constructDesignParams(), error, hasElapsedTime = true, hasStageTime = true, postStagesBlock, preStagesBlock, stageSpecificBlock, stageTracker, timerUnit = 'ms', title, }) { | ||
const { stdout } = useStdout(); | ||
const [levelOfCompaction, setLevelOfCompaction] = React.useState(determineCompactionLevel({ | ||
hasElapsedTime, | ||
hasStageTime, | ||
postStagesBlock, | ||
preStagesBlock, | ||
stageSpecificBlock, | ||
stageTracker, | ||
title, | ||
}, stdout.rows - 1, stdout.columns).compactionLevel); | ||
React.useEffect(() => { | ||
setLevelOfCompaction(determineCompactionLevel({ | ||
hasElapsedTime, | ||
hasStageTime, | ||
postStagesBlock, | ||
preStagesBlock, | ||
stageSpecificBlock, | ||
stageTracker, | ||
title, | ||
}, stdout.rows - 1, stdout.columns).compactionLevel); | ||
}, [ | ||
compactionLevel, | ||
hasElapsedTime, | ||
hasStageTime, | ||
postStagesBlock, | ||
preStagesBlock, | ||
stageSpecificBlock, | ||
stageTracker, | ||
stdout.columns, | ||
stdout.rows, | ||
title, | ||
]); | ||
React.useEffect(() => { | ||
const handler = () => { | ||
setLevelOfCompaction(determineCompactionLevel({ | ||
hasElapsedTime, | ||
hasStageTime, | ||
postStagesBlock, | ||
preStagesBlock, | ||
stageSpecificBlock, | ||
stageTracker, | ||
title, | ||
}, stdout.rows - 1, stdout.columns).compactionLevel); | ||
}; | ||
stdout.on('resize', handler); | ||
return () => { | ||
stdout.removeListener('resize', handler); | ||
}; | ||
}); | ||
// if compactionLevel is provided, use that instead of the calculated level | ||
const actualLevelOfCompaction = compactionLevel ?? levelOfCompaction; | ||
// filter out the info blocks based on the compaction level | ||
const preStages = filterInfos(preStagesBlock ?? [], actualLevelOfCompaction, 4); | ||
const postStages = filterInfos(postStagesBlock ?? [], actualLevelOfCompaction, 5); | ||
const stageSpecific = filterInfos(stageSpecificBlock ?? [], actualLevelOfCompaction, 7); | ||
// Reduce padding if the compaction level is 8 | ||
const padding = actualLevelOfCompaction === 8 ? 0 : 1; | ||
return (React.createElement(Box, { flexDirection: "column", marginTop: padding, marginBottom: padding }, | ||
actualLevelOfCompaction < 3 && title && (React.createElement(Box, { paddingBottom: padding }, | ||
React.createElement(Divider, { title: title, ...design.title, terminalWidth: stdout.columns }))), | ||
preStages && preStages.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 1, paddingBottom: padding }, | ||
React.createElement(Infos, { design: design, error: error, keyValuePairs: preStages }))), | ||
React.createElement(Box, { flexDirection: "column", marginLeft: 1, paddingBottom: padding }, | ||
React.createElement(StageEntries, { compactionLevel: actualLevelOfCompaction, design: design, error: error, hasStageTime: hasStageTime, stageSpecificBlock: stageSpecific, stageTracker: stageTracker, timerUnit: timerUnit })), | ||
postStages && postStages.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 1 }, | ||
React.createElement(Infos, { design: design, error: error, keyValuePairs: postStages }))), | ||
hasElapsedTime && (React.createElement(Box, { marginLeft: 1, display: actualLevelOfCompaction < 2 ? 'flex' : 'none' }, | ||
React.createElement(Text, null, "Elapsed Time: "), | ||
React.createElement(Timer, { unit: timerUnit }))))); | ||
} |
@@ -6,3 +6,3 @@ import { type SpinnerName } from 'cli-spinners'; | ||
/** | ||
* Icon to display for a completed stage. Defaults to '✔' | ||
* Icon to display for a completed stage. Defaults to green '✔' | ||
*/ | ||
@@ -17,11 +17,11 @@ completed?: IconProps; | ||
/** | ||
* Icon to display for a failed stage. Defaults to '✘' | ||
* Icon to display for a failed stage. Defaults to red '✘' | ||
*/ | ||
failed?: IconProps; | ||
/** | ||
* Icon to display for a pending stage. Defaults to '◼' | ||
* Icon to display for a pending stage. Defaults to dim '◼' | ||
*/ | ||
pending?: IconProps; | ||
/** | ||
* Icon to display for a skipped stage. Defaults to '◯' | ||
* Icon to display for a skipped stage. Defaults to dim '◯' | ||
*/ | ||
@@ -33,2 +33,14 @@ skipped?: IconProps; | ||
info?: IconProps; | ||
/** | ||
* Icon to display for a aborted stage. Defaults to red '◼' | ||
*/ | ||
aborted?: IconProps; | ||
/** | ||
* Icon to display for a paused stage. Defaults to magenta '●' | ||
*/ | ||
paused?: IconProps; | ||
/** | ||
* Icon to display for an async stage. Defaults to magenta '▶' | ||
*/ | ||
async?: IconProps; | ||
}; | ||
@@ -35,0 +47,0 @@ title?: { |
@@ -5,2 +5,16 @@ import figures from 'figures'; | ||
icons: { | ||
aborted: { | ||
color: 'red', | ||
figure: figures.squareSmallFilled, | ||
paddingLeft: 0, | ||
paddingRight: 0, | ||
...design?.icons?.current, | ||
}, | ||
async: { | ||
color: 'magenta', | ||
figure: figures.play, | ||
paddingLeft: 0, | ||
paddingRight: 0, | ||
...design?.icons?.current, | ||
}, | ||
completed: { | ||
@@ -34,2 +48,9 @@ color: 'green', | ||
}, | ||
paused: { | ||
color: 'magenta', | ||
figure: figures.bullet, | ||
paddingLeft: 0, | ||
paddingRight: 1, | ||
...design?.icons?.current, | ||
}, | ||
pending: { | ||
@@ -36,0 +57,0 @@ color: 'dim', |
@@ -55,3 +55,3 @@ import { KeyValuePair, SimpleMessage, StageInfoBlock } from './components/stages.js'; | ||
export declare class MultiStageOutput<T extends Record<string, unknown>> implements Disposable { | ||
private ciInstance; | ||
private readonly ciInstance; | ||
private data?; | ||
@@ -61,3 +61,3 @@ private readonly design; | ||
private readonly hasStageTime?; | ||
private inkInstance; | ||
private readonly inkInstance; | ||
private readonly postStagesBlock?; | ||
@@ -67,3 +67,3 @@ private readonly preStagesBlock?; | ||
private readonly stageSpecificBlock?; | ||
private stageTracker; | ||
private readonly stageTracker; | ||
private stopped; | ||
@@ -70,0 +70,0 @@ private readonly timerUnit?; |
@@ -80,3 +80,3 @@ import { ux } from '@oclif/core/ux'; | ||
this.startTimes.set(stage, Date.now()); | ||
ux.stdout(`${this.design.icons.current} ${capitalCase(stage)}...`); | ||
ux.stdout(`${this.design.icons.current.figure} ${capitalCase(stage)}...`); | ||
this.printInfo(this.preStagesBlock, 3); | ||
@@ -95,3 +95,3 @@ this.printInfo(this.stageSpecificBlock?.filter((info) => info.stage === stage), 3); | ||
const displayTime = readableTime(elapsedTime, this.timerUnit); | ||
ux.stdout(`${this.design.icons[status]} ${capitalCase(stage)} (${displayTime})`); | ||
ux.stdout(`${this.design.icons[status].figure} ${capitalCase(stage)} (${displayTime})`); | ||
this.printInfo(this.preStagesBlock, 3); | ||
@@ -102,6 +102,6 @@ this.printInfo(this.stageSpecificBlock?.filter((info) => info.stage === stage), 3); | ||
else if (status === 'skipped') { | ||
ux.stdout(`${this.design.icons[status]} ${capitalCase(stage)} - Skipped`); | ||
ux.stdout(`${this.design.icons[status].figure} ${capitalCase(stage)} - Skipped`); | ||
} | ||
else { | ||
ux.stdout(`${this.design.icons[status]} ${capitalCase(stage)}`); | ||
ux.stdout(`${this.design.icons[status].figure} ${capitalCase(stage)}`); | ||
this.printInfo(this.preStagesBlock, 3); | ||
@@ -270,4 +270,4 @@ this.printInfo(this.stageSpecificBlock?.filter((info) => info.stage === stage), 3); | ||
const error = finalStatus === 'failed' ? new Error('Error') : undefined; | ||
const stagesInput = { ...this.generateStagesInput(), ...(error ? { error } : {}) }; | ||
this.inkInstance?.rerender(React.createElement(Stages, { ...stagesInput })); | ||
const stagesInput = { ...this.generateStagesInput({ compactionLevel: 0 }), ...(error ? { error } : {}) }; | ||
this.inkInstance?.rerender(React.createElement(Stages, { ...stagesInput, compactionLevel: 0 })); | ||
this.inkInstance?.unmount(); | ||
@@ -296,2 +296,3 @@ } | ||
isBold: info.bold, | ||
neverCollapse: info.neverCollapse, | ||
type: info.type, | ||
@@ -305,4 +306,6 @@ value: formattedData, | ||
/** shared method to populate everything needed for Stages cmp */ | ||
generateStagesInput() { | ||
generateStagesInput(opts) { | ||
const { compactionLevel } = opts ?? {}; | ||
return { | ||
compactionLevel, | ||
design: this.design, | ||
@@ -309,0 +312,0 @@ hasElapsedTime: this.hasElapsedTime, |
@@ -1,3 +0,4 @@ | ||
export type StageStatus = 'pending' | 'current' | 'completed' | 'skipped' | 'failed'; | ||
export type StageStatus = 'aborted' | 'async' | 'completed' | 'current' | 'failed' | 'paused' | 'pending' | 'skipped'; | ||
export declare class StageTracker { | ||
private stages; | ||
current: string | undefined; | ||
@@ -7,4 +8,10 @@ private map; | ||
constructor(stages: readonly string[] | string[]); | ||
get size(): number; | ||
entries(): IterableIterator<[string, StageStatus]>; | ||
get(stage: string): StageStatus | undefined; | ||
getCurrent(): { | ||
stage: string; | ||
status: StageStatus; | ||
} | undefined; | ||
indexOf(stage: string): number; | ||
refresh(nextStage: string, opts?: { | ||
@@ -11,0 +18,0 @@ finalStatus?: StageStatus; |
import { Performance } from '@oclif/core/performance'; | ||
export class StageTracker { | ||
stages; | ||
current; | ||
@@ -7,4 +8,8 @@ map = new Map(); | ||
constructor(stages) { | ||
this.stages = stages; | ||
this.map = new Map(stages.map((stage) => [stage, 'pending'])); | ||
} | ||
get size() { | ||
return this.map.size; | ||
} | ||
entries() { | ||
@@ -16,2 +21,13 @@ return this.map.entries(); | ||
} | ||
getCurrent() { | ||
if (this.current) { | ||
return { | ||
stage: this.current, | ||
status: this.map.get(this.current), | ||
}; | ||
} | ||
} | ||
indexOf(stage) { | ||
return this.stages.indexOf(stage); | ||
} | ||
refresh(nextStage, opts) { | ||
@@ -18,0 +34,0 @@ const stages = [...this.map.keys()]; |
{ | ||
"name": "@oclif/multi-stage-output", | ||
"description": "Terminal output for oclif commands with multiple stages", | ||
"version": "0.4.0", | ||
"version": "0.4.1-dev.0", | ||
"author": "Salesforce", | ||
@@ -6,0 +6,0 @@ "bugs": "https://github.com/oclif/multi-stage-output/issues", |
63483
1342