Programmatic API
Everything the CLI does is exported from @noctcore/showcase-kit:
import { capture, exportPortfolio, frame, generateIcons, hero, loadConfig, readmeSnippet } from '@noctcore/showcase-kit';
const config = await loadConfig(); // or loadConfig('path/to/showcase.config.mjs')await capture(config, { only: ['library'], langs: ['en'] });await frame(config);await exportPortfolio(config);console.log(readmeSnippet(config, { lang: 'en', cols: 2 }));await hero(config);await generateIcons('mascot.png', 'web', 'public');resolveConfig(object, rootDir) validates a config object without a file, with the same checks as the CLI (see
Config). defineConfig only exists for types and editor completion: it reads
target.mode, so tty setup and nav functions get the terminal session instead of a page. A resolved config is
a web or a tty config; isTtyConfig(config) tells them apart.
The terminal engine is exported too, for scripts of your own: openTtySession({ command, cwd, env, cols, rows })
returns a session with press, type, waitForText, screen and close, and
renderTtyScreen(page, session.screen(), resolveTerminalOptions(undefined, cwd), 2) renders a screen to a PNG in a
Playwright page whose context has the same device scale factor. parseKeys, DARK_THEME, LIGHT_THEME and
TERMINAL_DEFAULTS come with them.
record(config, { only, langs }) records clips, and encodeAnimation(frames, { format, loop, quality, fps }) is
the encoder behind it: frames of one size as { png, delayMs } in, an animated WebP (lossless, or lossy with
quality), a GIF, or an MP4 through ffmpeg out. A delay longer than one WebP or GIF frame can hold (65535 ms in
sharp) is split into repeats of the same image, and input it cannot encode is refused with a ShowcaseError.
Errors meant for the person running a command are ShowcaseErrors; an invalid config throws a ConfigError, a
ShowcaseError whose issues lists every problem.
The rest of this page is generated from src/index.ts on each build: every export, whether it exists at runtime
or only as a type, its summary from the source and its signature.
Config
Section titled “Config”CONFIG_NAMES
const CONFIG_NAMES: readonly ["showcase.config.ts", "showcase.config.mts", "showcase.config.mjs", "showcase.config.js"]defineConfig
target.mode, so in tty mode setup and function navs get the terminal session instead of a page.function defineConfig<M extends Mode = "url" | "cdp">(config: ShowcaseConfig<M> & { target: { mode: M; }; }): ShowcaseConfig<M>findConfigFile
from or a parent directory, stopping at the first directory that holds a package.json or .git (the project root): a config above it belongs to some other project.function findConfigFile(from?: string): string | undefinedinit
dir. Refuses to overwrite an existing config unless force.function init(dir: string, { typescript, force, tty }?: { typescript?: boolean | undefined; force?: boolean | undefined; tty?: boolean | undefined; }): stringisTtyConfig
function isTtyConfig(config: ResolvedConfig): config is ResolvedTtyConfigloadConfig
file, search from cwd upward.function loadConfig(file?: string | undefined, cwd?: string): Promise<ResolvedConfig>resolveConfig
function resolveConfig(input: unknown, root: string, source?: string | undefined): ResolvedConfigstarterConfig
dir where possible.function starterConfig(dir: string, typescript: boolean, { tty }?: { tty?: boolean | undefined; }): stringPipeline
Section titled “Pipeline”capture
function capture(config: ResolvedConfig, options?: CaptureOptions): Promise<CaptureResult>frame
outputs.readme.function frame(config: ResolvedConfig, options?: FrameRunOptions): Promise<FramedFile[]>record
function record(config: ResolvedConfig, options?: RecordOptions): Promise<RecordedClip[]>CapturedFile
interface CapturedFile { lang: string; id: string; path: string; width: number; height: number;}CaptureOptions
interface CaptureOptions { only?: string[]; langs?: string[];}CaptureResult
interface CaptureResult { files: CapturedFile[]; startedPid?: number;}FramedFile
interface FramedFile { lang: string; id: string; path: string; width: number; height: number;}FrameRunOptions
interface FrameRunOptions { only?: string[]; langs?: string[];}RecordedClip
interface RecordedClip { lang: string; id: string; width: number; height: number; frames: number; durationMs: number; files: RecordedFile[];}RecordedFile
interface RecordedFile { format: ClipFormat; path: string; bytes: number;}RecordOptions
interface RecordOptions { only?: string[]; langs?: string[];}Outputs
Section titled “Outputs”encodeIcns
function encodeIcns(images: { type: string; data: Buffer<ArrayBufferLike>; }[]): Buffer<ArrayBufferLike>encodeIco
function encodeIco(images: { size: number; data: Buffer<ArrayBufferLike>; }[]): Buffer<ArrayBufferLike>exportPortfolio
size) portfolio images: the framed window contained on the background at exactly size pixels, a thumbnail copy, and a gallery JSON the portfolio can import.function exportPortfolio(config: ResolvedConfig, options?: { only?: string[] | undefined; }): Promise<PortfolioResult>GALLERY_FILE
const GALLERY_FILE: "showcase.gallery.json"generateIcons
preset from one square source image.function generateIcons(source: string, preset: IconPreset, outDir: string): Promise<{ file: string; path: string; }[]>hero
hero.output at exactly hero.size pixels.function hero(config: ResolvedConfig): Promise<{ path: string; width: number; height: number; }>heroHtml
hero.layout.function heroHtml(config: ResolvedConfig, { windows, logo }: { windows: HeroWindow[]; logo: string | undefined; }): stringICON_PRESETS
const ICON_PRESETS: Record<IconPreset, IconFile[]>readmeSnippet
<sub> captions in the next, ready to paste into a README. Clips follow the shots: an <img> of the WebP (or GIF), with a link to the MP4 when there is one, or only a link for an MP4-only clip.function readmeSnippet(config: ResolvedConfig, options?: ReadmeOptions): stringGalleryItem
GalleryItem type.interface GalleryItem { src: string; alt: string; caption: string;}IconPreset
type IconPreset = 'web' | 'electron' | 'tauri';PortfolioResult
interface PortfolioResult { dir: string; files: string[]; thumbnail: string | undefined; gallery: GalleryItem[]; galleryFile: string | undefined;}ReadmeLayout
showcase readme lays out the images. Every layout uses only HTML that GitHub keeps in a README.type ReadmeLayout = 'table' | 'rows' | 'featured' | 'details' | 'list';ReadmeOptions
interface ReadmeOptions { lang?: string; layout?: ReadmeLayout; cols?: number; base?: string; only?: string[];}Terminal engine
Section titled “Terminal engine”DARK_THEME
const DARK_THEME: TerminalThemeLIGHT_THEME
const LIGHT_THEME: TerminalThemeopenTtySession
const openTtySession: OpenTtySessionparseKeys
Keys into keystrokes: one string of bytes per key, to be written one at a time. Plain text is typed a character at a time; {Name} is a key (see KNOWN); {{ is a literal {. Unknown names are errors.function parseKeys(keys: Keys, opts?: KeyOptions): string[]renderTtyScreen
deviceScaleFactor.const renderTtyScreen: RenderTtyScreenresolveTerminalOptions
baseDir, the config's folder). It checks what the renderer relies on (16 ansi colors, #rrggbb colors, positive sizes, font files that exist) and throws a ShowcaseError otherwise; the config layer still owns shape checks such as unknown keys.function resolveTerminalOptions(opts: TerminalOptions | undefined, baseDir: string): ResolvedTerminalOptionsTERMINAL_DEFAULTS
const TERMINAL_DEFAULTS: { readonly size: 15; readonly lineHeight: 1.32; readonly padding: 12; readonly cursor: "hide"; }Encoding
Section titled “Encoding”encodeAnimation
ffmpeg from PATH).function encodeAnimation(frames: AnimationFrame[], opts: EncodeOptions): Promise<Buffer<ArrayBufferLike>>AnimationFormat
type AnimationFormat = 'webp' | 'gif' | 'mp4';AnimationFrame
interface AnimationFrame { png: Buffer; delayMs: number;}EncodeOptions
interface EncodeOptions { format: AnimationFormat; loop?: number; quality?: number; fps?: number;}Errors
Section titled “Errors”ConfigError
class ConfigError extends ShowcaseError { constructor(issues: string[], source?: string | undefined); name: string; readonly issues: string[];}ShowcaseError
class ShowcaseError extends Error { constructor(message?: string | undefined); constructor(message?: string | undefined, options?: ErrorOptions | undefined); name: string;}The config types behind Config, their resolved forms, and the terminal engine’s contract.
Background
type Background = string | { type: 'solid'; color: string;} | { type: 'gradient'; from: string; to: string; angle?: number;} | { type: 'transparent';} | { type: 'mesh'; colors: string[];} | { type: 'dots'; color: string; dot?: string; spacing?: number;} | { type: 'noise'; from: string; to: string; angle?: number; amount?: number;};BrowserOptions
interface BrowserOptions { channel?: string; executablePath?: string; headless?: boolean; args?: string[];}CdpTarget
interface CdpTarget { mode: 'cdp'; cdpUrl?: string; pageMatch?: string | RegExp; start?: string; cwd?: string; env?: Record<string, string>; readyTimeoutMs?: number;}Clip
showcase record. Each clip starts a fresh app.interface Clip { id: string; title?: string; caption?: string; alt?: string; steps: ClipStep[]; fps?: number; durationMs?: number; maxFrames?: number; tailMs?: number; formats?: ClipFormat[];}ClipFormat
type ClipFormat = 'webp' | 'gif' | 'mp4';ClipStep
type ClipStep = { keys: Keys;} | { type: string; delayMs?: number;} | { waitFor: string | RegExp;} | { sleep: number;};CommonConfig
interface CommonConfig { name: string; slug?: string; root?: string; deviceScaleFactor?: number; langs?: string[]; frame?: FrameOptions; outputs?: Outputs; hero?: HeroOptions; browser?: BrowserOptions; timeouts?: Timeouts;}FrameOptions
interface FrameOptions { style?: FrameStyle; theme?: 'light' | 'dark'; title?: string | false; address?: string; background?: Background; padding?: number; radius?: number; shadow?: boolean; quality?: number; maxWidth?: number;}FrameStyle
window reads as macOS, windows as Windows 11, browser as a browser with an address bar, and terminal as a terminal emulator.type FrameStyle = 'window' | 'minimal' | 'none' | 'browser' | 'windows' | 'terminal';HeroLayout
type HeroLayout = 'stack' | 'spotlight' | 'split' | 'row' | 'mosaic' | 'centered';HeroOptions
showcase hero renders.interface HeroOptions { layout?: HeroLayout; tagline?: string; logo?: string; shots?: string[]; lang?: string; output?: string; size?: [ number, number ]; background?: Background; theme?: 'light' | 'dark'; quality?: number;}Keys
{Enter}, {Down}, {Tab}, {Esc}, {C-c}.type Keys = string | string[];Mode
'url', 'cdp' or 'tty'.type Mode = Target['mode'];Nav
type Nav = string | { click: string;} | { goto: string;} | NavFn;NavFn
type NavFn = (page: Page) => Promise<void> | void;OpenTtySession
type OpenTtySession = (opts: TtySessionOptions) => Promise<TtySession>;Outputs
interface Outputs { raw?: string; readme?: string | false; portfolio?: PortfolioOutput; clips?: string;}PortfolioOutput
interface PortfolioOutput { dir: string; size?: [ number, number ]; format?: 'webp' | 'png'; quality?: number; thumbnail?: string; lang?: string; publicPath?: string; padding?: number; gallery?: string | false;}RenderTtyScreen
deviceScaleFactor. The PNG is the terminal area: grid plus padding on the theme background, ready for frame, portfolio and hero.type RenderTtyScreen = (page: Page, screen: TtyScreen, look: ResolvedTerminalOptions, deviceScaleFactor: number) => Promise<Buffer>;ResolvedBackground
angle filled in.type ResolvedBackground = { type: 'solid'; color: string;} | { type: 'gradient'; from: string; to: string; angle: number;} | { type: 'transparent';} | { type: 'mesh'; colors: string[];} | { type: 'dots'; color: string; dot: string; spacing: number;} | { type: 'noise'; from: string; to: string; angle: number; amount: number;};ResolvedClip
interface ResolvedClip { id: string; title: string; caption: string | undefined; alt: string; steps: ClipStep[]; fps: number; durationMs: number | undefined; maxFrames?: number; tailMs: number; formats: ClipFormat[];}ResolvedConfig
target.mode.type ResolvedConfig = ResolvedWebConfig | ResolvedTtyConfig;ResolvedFrame
frame with every default filled in.interface ResolvedFrame { style: FrameStyle; theme: 'light' | 'dark'; title: string | false; address: string; background: ResolvedBackground; padding: number; radius: number; shadow: boolean; quality: number; maxWidth: number | undefined;}ResolvedHero
hero with every default filled in.interface ResolvedHero { layout: HeroLayout; tagline: string | undefined; logo: string | undefined; shots: string[]; lang: string; output: string; size: [ number, number ]; background: ResolvedBackground; theme: 'light' | 'dark'; quality: number;}ResolvedPortfolio
outputs.portfolio with every default filled in.interface ResolvedPortfolio { dir: string; size: [ number, number ]; format: 'webp' | 'png'; quality: number; thumbnail: string; lang: string; publicPath: string; padding: number; gallery: string | false;}ResolvedShot
id, title, alt, caption.type ResolvedShot = ResolvedWebShot | ResolvedTtyShot;ResolvedTerminalOptions
TerminalOptions with every default filled in and every file path absolute.interface ResolvedTerminalOptions { theme: TerminalTheme; font: { file?: string; boldFile?: string; italicFile?: string; boldItalicFile?: string; fallbackFile?: string; size: number; }; lineHeight: number; padding: number; cursor: 'hide' | 'show';}ResolvedTtyConfig
interface ResolvedTtyConfig extends ResolvedCommon { target: ResolvedTtyTarget; ready: string | RegExp | undefined; terminal: ResolvedTerminalOptions; setup: TtyConfig['setup']; shots: ResolvedTtyShot[]; clips: ResolvedClip[];}ResolvedTtyShot
interface ResolvedTtyShot extends TtyShot { title: string; alt: string; delayMs: number; restart: boolean;}ResolvedTtyTarget
cwd absolute.interface ResolvedTtyTarget { mode: 'tty'; command: string | [ file: string, ...args: string[] ]; cwd: string; env: TtyTarget['env']; inheritEnv: boolean | string[]; cols: number; rows: number; quitKey: string | false; inputDelayMs: number; readyTimeoutMs: number;}ResolvedWebConfig
interface ResolvedWebConfig extends ResolvedCommon { target: WebTarget & { readyTimeoutMs: number; }; ready: string | undefined; viewport: { width: number; height: number; }; colorScheme: 'light' | 'dark' | 'no-preference'; css: string | undefined; setup: WebConfig['setup']; shots: ResolvedWebShot[];}ResolvedWebShot
interface ResolvedWebShot extends Shot { title: string; alt: string; delayMs: number;}SetupContext
setup receives in url and cdp mode.interface SetupContext { page: Page; context: BrowserContext; lang: string; mode: WebTarget['mode']; config: ResolvedWebConfig;}Shot
interface Shot { id: string; title?: string; caption?: string; alt?: string; nav?: Nav; waitFor?: string; delayMs?: number;}ShowcaseConfig
showcase.config.* file exports. Without a type argument it is the web config, as before tty mode; defineConfig picks the right one from target.mode.type ShowcaseConfig<M extends Mode = WebTarget['mode']> = M extends 'tty' ? TtyConfig : WebConfig;Target
mode.type Target = UrlTarget | CdpTarget | TtyTarget;TerminalOptions
interface TerminalOptions { theme?: 'dark' | 'light' | TerminalTheme; font?: { file?: string; boldFile?: string; italicFile?: string; boldItalicFile?: string; fallbackFile?: string; size?: number; }; lineHeight?: number; padding?: number; cursor?: 'hide' | 'show';}TerminalTheme
interface TerminalTheme { background: string; foreground: string; cursor?: string; ansi: string[];}Timeouts
interface Timeouts { readyMs?: number; shotMs?: number; networkIdleMs?: number;}TtyConfig
mode: 'tty'.interface TtyConfig extends CommonConfig { target: TtyTarget; ready?: string | RegExp; terminal?: TerminalOptions; setup?: (ctx: TtySetupContext) => Promise<void> | void; shots: TtyShot[]; clips?: Clip[];}TtyNavFn
type TtyNavFn = (tty: TtySession) => Promise<void> | void;TtyScreen
interface TtyScreen { cols: number; rows: number; text: string; key: string; readonly grid: unknown;}TtySession
interface TtySession { readonly pid: number; press(keys: Keys): Promise<void>; type(text: string, opts?: { delayMs?: number; }): Promise<void>; waitForText(pattern: string | RegExp, opts?: { timeoutMs?: number; }): Promise<void>; screenText(): string; screen(): TtyScreen; resize(cols: number, rows: number): Promise<void>; sleep(ms: number): Promise<void>; close(opts?: { quitKey?: string | false; }): Promise<void>; readonly exited: Promise<number | null>;}TtySessionOptions
interface TtySessionOptions { command: string | [ file: string, ...args: string[] ]; cwd: string; env: Record<string, string>; inheritEnv?: boolean | string[]; cols: number; rows: number;}TtySetupContext
setup receives in tty mode.interface TtySetupContext { tty: TtySession; lang: string; mode: 'tty'; config: ResolvedTtyConfig;}TtyShot
interface TtyShot { id: string; title?: string; caption?: string; alt?: string; keys?: Keys; nav?: TtyNavFn; waitFor?: string | RegExp; delayMs?: number; restart?: boolean;}TtyTarget
@lydell/node-pty (or node-pty).interface TtyTarget { mode: 'tty'; command: string | [ file: string, ...args: string[] ]; cwd?: string; env?: Record<string, string> | ((ctx: { lang: string; }) => Record<string, string>); inheritEnv?: boolean | string[]; cols?: number; rows?: number; quitKey?: string | false; inputDelayMs?: number; readyTimeoutMs?: number;}UrlTarget
interface UrlTarget { mode: 'url'; url: string; start?: string; cwd?: string; env?: Record<string, string>; readyTimeoutMs?: number; reuseExisting?: boolean;}WebConfig
mode: 'url' or mode: 'cdp'.interface WebConfig extends CommonConfig { target: WebTarget; ready?: string; viewport?: { width: number; height: number; }; colorScheme?: 'light' | 'dark' | 'no-preference'; css?: string; setup?: (ctx: SetupContext) => Promise<void> | void; shots: Shot[];}WebTarget
type WebTarget = UrlTarget | CdpTarget;