Skip to content

Commit

Permalink
feat: compute content height
Browse files Browse the repository at this point in the history
  • Loading branch information
hi-ogawa committed Dec 24, 2023
1 parent b6135a5 commit 4c44479
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 44 deletions.
31 changes: 13 additions & 18 deletions packages/utils-node/src/prompt-utils.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
// https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences
// https://github.com/nodejs/node/blob/a717fa211194b735cdfc1044c7351c6cf96b8783/lib/internal/readline/utils.js
// https://github.com/terkelg/sisteransi/blob/e219f8672540dde6e92a19e48a3567ef2548d620/src/index.js
export const ESC = `\x1b`;
export const CSI = `${ESC}[`;
export const kClearToLineBeginning = `${CSI}1K`;
export const kClearToLineEnd = `${CSI}1K`;
export const kClearLine = `${CSI}2K`;
export const kClearScreenDown = `${CSI}0J`;
export const kClearAll = `${CSI}2J`;
export const CSI = "\x1b[";

export function cursorTo(x: number, y?: number) {
return typeof y !== "number" ? `${CSI}${x + 1}G` : `${CSI}${y + 1};${x + 1}H`;
// cf. https://github.com/terkelg/prompts/blob/735603af7c7990ac9efcfba6146967a7dbb15f50/lib/util/clear.js#L5-L6
export function computeHeight(s: string, width: number) {
// strip CSI
const s2 = s.replaceAll(/\x1b\[\d*[A-Za-z]/g, "");

// check line wrap
const wraps = s2
.split("\n")
.map((line) => computeLineWrap(line.length, width));
return wraps.reduce((x, y) => x + y);
}

export function moveCursor(dx: number, dy: number) {
return [
dx < 0 && `${CSI}${-dx}D`,
dx > 0 && `${CSI}${dx}C`,
dy < 0 && `${CSI}${-dy}A`,
dy > 0 && `${CSI}${dy}B`,
]
.filter(Boolean)
.join("");
function computeLineWrap(l: number, w: number) {
return Math.floor(Math.max((l - 1) / w, 0) + 1);
}
50 changes: 24 additions & 26 deletions packages/utils-node/src/prompt.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import readline from "node:readline";
import { promisify } from "node:util";
import { createManualPromise } from "@hiogawa/utils";
import { CSI, cursorTo } from "./prompt-utils";
import { CSI, computeHeight } from "./prompt-utils";

// cf. https://github.com/google/zx/blob/956dcc3bbdd349ac4c41f8db51add4efa2f58456/src/goods.ts#L83
export async function promptQuestion(query: string): Promise<string> {
Expand All @@ -27,38 +28,36 @@ export async function promptAutocomplete(config: {
message: string;
loadOptions: (input: string) => Promise<string[]>;
}): Promise<AutocompleteResult> {
const write = promisify(process.stdout.write.bind(process.stdout));
const manual = createManualPromise<AutocompleteResult>();
let input = "";
let output = "";

async function write(s: string) {
const manual = createManualPromise<void>();
process.stdout.write(s, (err) =>
err ? manual.reject(err) : manual.resolve()
);
await manual;
}

// TODO: async handler race condition
async function render() {
async function render(renderOptions?: { done?: boolean }) {
const options = await config.loadOptions(input);

// TODO: pagination based on process.stdout.rows?
const part1 = config.message + " > " + input;
if (renderOptions?.done) {
await write(part1);
return;
}

// TODO: pagination based on process.stdout.rows?
const part2 = options
.slice(0, 20)
.map((v) => ` ${v}\n`)
.join("");
const content = [part1, "\n", part2].join("");

// simple clear full screen
output = [`${CSI}2J`, cursorTo(0, 0), part1, "\n", part2].join("");
await write(output);

// TODO: keep track of last output and clear only previously rendered lines
// (this is what prompts, clack, etc.. does)
// process.stdout.columns;
// 1. clean screen below
// 2. write content
// 3. reset cursor position
const height = computeHeight(content, process.stdout.columns);
await write(
[`${CSI}0J`, content, `${CSI}${height - 1}A`, `${CSI}1G`].join("")
);
}

// TODO: async handler race condition
const dispose = setupKeypressHandler(async (str: string, key: KeyInfo) => {
// TODO: more special keys
// https://github.com/terkelg/prompts/blob/735603af7c7990ac9efcfba6146967a7dbb15f50/lib/util/action.js#L18-L26
Expand All @@ -75,18 +74,17 @@ export async function promptAutocomplete(config: {
} else {
input += str;
}
render();
await render();
});

try {
// await write(`${CSI}?25l`); // hide cursor
render();
await write(`${CSI}?25l`); // hide cursor
await render();
return await manual.promise;
} finally {
// TODO: render final result
// render()
await render({ done: true });
await write(`${CSI}0J`); // clean below
// await write(`${CSI}?25h`); // show cursor
await write(`${CSI}?25h`); // show cursor
dispose();
}
}
Expand Down

0 comments on commit 4c44479

Please # to comment.