diff --git a/src/commands/check/interactive.ts b/src/commands/check/interactive.ts index 732b199..7eb273a 100644 --- a/src/commands/check/interactive.ts +++ b/src/commands/check/interactive.ts @@ -7,7 +7,7 @@ import { createControlledPromise, notNullish } from '@antfu/utils' import type { CheckOptions, InteractiveContext, PackageMeta, ResolvedDepChange } from '../../types' import { getVersionOfRange, updateTargetVersion } from '../../io/resolves' import { getPrefixedVersion } from '../../utils/versions' -import { FIG_BLOCK, FIG_NO_POINTER, FIG_POINTER, colorizeVersionDiff, formatTable } from '../../render' +import { FIG_BLOCK, FIG_NO_POINTER, FIG_POINTER, colorizeVersionDiff, createSliceRender, formatTable } from '../../render' import { timeDifference } from '../../utils/time' import { renderChanges } from './render' @@ -56,19 +56,18 @@ export async function promptInteractive(pkgs: PackageMeta[], options: CheckOptio return { render() { + const sr = createSliceRender() const Y = (v: string) => c.bold(c.green(v)) console.clear() - console.log(`${FIG_BLOCK} ${c.gray(`${Y('↑↓')} to select, ${Y('space')} to toggle, ${Y('→')} to change version`)}`) - console.log(`${FIG_BLOCK} ${c.gray(`${Y('enter')} to confirm, ${Y('esc')} to cancel`)}`) - console.log() - - const lines: string[] = [] + sr.push({ content: `${FIG_BLOCK} ${c.gray(`${Y('↑↓')} to select, ${Y('space')} to toggle, ${Y('→')} to change version`)}`, fixed: true }) + sr.push({ content: `${FIG_BLOCK} ${c.gray(`${Y('enter')} to confirm, ${Y('esc')} to cancel`)}`, fixed: true }) + sr.push({ content: '', fixed: true }) pkgs.forEach((pkg) => { - lines.push(...renderChanges(pkg, options, ctx).lines) + sr.push(...renderChanges(pkg, options, ctx).lines.map(x => ({ content: x }))) }) - console.log(lines.join('\n')) + sr.render(index) }, onKey(key) { switch (key.name) { diff --git a/src/render.ts b/src/render.ts index 3a0d65b..3d31fec 100644 --- a/src/render.ts +++ b/src/render.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-console */ +import process from 'node:process' import c from 'picocolors' import { SemVer } from 'semver' import { getDiff } from './io/resolves' @@ -110,3 +112,77 @@ export function colorizeVersionDiff(from: string, to: string, hightlightRange = + middot + c[color](partsToColor.slice(i).join('.')).trim() } + +interface SliceRenderLine { + content: string + fixed?: boolean +} + +export function createSliceRender() { + const buffer: SliceRenderLine[] = [] + + return { + push(...lines: SliceRenderLine[]) { + buffer.push(...lines) + }, + render(selectedDepIndex: number) { + let { + rows: remainHeight, + columns: availableWidth, + } = process.stdout + + const lines: SliceRenderLine[] = buffer.length < remainHeight - 1 + ? buffer + : [...buffer, { content: c.yellow(' -- END --') }] + + // spare space for cursor + remainHeight -= 1 + let i = 0 + while (i < lines.length) { + const curr = lines[i] + if (curr.fixed) { + console.log(curr.content) + remainHeight -= 1 + i++ + } + else { + break + } + } + + const remainLines = lines.slice(i) + + // calculate focused line index from selected dep index + let focusedLineIndex = 0 + let depIndex = 0 + for (const line of remainLines) { + if (line.content.includes(FIG_CHECK)) + depIndex += 1 + + if (depIndex === selectedDepIndex) + break + else + focusedLineIndex += 1 + } + + let slice: SliceRenderLine[] + if ( + remainHeight < 1 + || remainLines.length === 0 + || remainLines.length <= remainHeight + || lines.some(x => Math.ceil(visualLength(x.content) / availableWidth) > 1) + ) { + slice = remainLines + } + else { + const half = Math.floor((remainHeight - 1) / 2) + const f = focusedLineIndex - half + const b = focusedLineIndex + remainHeight - half - remainLines.length + const start = Math.max(0, b <= 0 ? f : f - b) + slice = remainLines.slice(start, start + remainHeight) + } + + console.log(slice.map(x => x.content).join('\n')) + }, + } +}