Skip to content

Commit

Permalink
Added custom select options, fixed some display/erase messages bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
Tpleme committed Jan 22, 2024
1 parent 8d427f3 commit 4cd2a34
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 67 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Code quality
run-name: Checking code quality from ${{ github.actor }} pushto master.
run-name: Checking code quality from ${{ github.actor }} push to master.

on:
push:
Expand Down
12 changes: 6 additions & 6 deletions commands/add.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default async (cmd, opts, appDir) => {

if (!appPath) {
await promisifyQuestion(
`❔ Where do you want to create the app ${appName}? (leave empty for current path)\n`,
`❔ Where do you want to create the app ${appName}? (leave empty for current path)`,
).then(path => {
if (path === "." || path.length === 0) {
appPath = "./";
Expand All @@ -51,11 +51,12 @@ export default async (cmd, opts, appDir) => {
return;
}

//TODO: build a object with all dev envs, each with frameworks and each framework with type
try {
if (!devEnv) {
await select({
message: "❔ Do you want to install any development environment?",
choices: [
question: "❔ Do you want to install any development environment?",
options: [
{ name: "Vite", value: "vite" },
{ name: "None", value: "none" },
],
Expand All @@ -69,10 +70,9 @@ export default async (cmd, opts, appDir) => {
handleError(`${devEnv} is not supported dev environment. Use vite instead.`, `${appPath}${appName}`);
return;
}

const framework = await select({
message: `❔ Pick a framework for the app ${appName}?`,
choices: [
question: `❔ Pick a framework for the app ${appName}?`,
options: [
{ name: "Vue", value: "vue", description: `Build ${appName} using Vue` },
{ name: "React", value: "react", description: `Build ${appName} using React` },
{ name: "Preact", value: "preact", description: `Build ${appName} using Preact` },
Expand Down
22 changes: 11 additions & 11 deletions commands/create.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { existsSync, rmSync } from "fs";
import { promisifyQuestion, rl } from "../utils/PromisifyInput.js";
import { promisifyQuestion } from "../utils/PromisifyInput.js";
import { promises as fsPromises } from "fs";
import { createDirectory, runCommandOnFolder } from "../cli_commands.js";
import { startAnimation, stopAnimation, setMessage } from "../utils/TerminalLoaderIndicator.js";
Expand All @@ -15,7 +15,7 @@ export default async (cmd, opts, appDir) => {

try {
if (!projectName) {
await promisifyQuestion("❔ What is the name of the project?\n").then(name => {
await promisifyQuestion("❔ What is the name of the project?").then(name => {
projectName = name;
});
}
Expand All @@ -29,7 +29,7 @@ export default async (cmd, opts, appDir) => {

if (!projectPath) {
await promisifyQuestion(
`❔ Where do you want to create the project ${projectName}? (leave empty for current path)\n`,
`❔ Where do you want to create the project ${projectName}? (leave empty for current path)`,
).then(path => {
if (path === "." || path.length === 0) {
projectPath = "./";
Expand All @@ -51,15 +51,15 @@ export default async (cmd, opts, appDir) => {

await runCommandOnFolder(`${projectPath}${projectName}`, "npm init -y");

console.log(okStyle(`✅ Project ${projectName} will be create on ${projectPath}${projectName}.`));
process.stdout.write(okStyle(`✅ Project ${projectName} will be create on ${projectPath}${projectName}.\n`));
} catch (err) {
handleError(err);
return;
}

try {
const folders = await promisifyQuestion(
"❔ Which apps should this project have? (ex: client,backoffice,server) (separate by comma)\n",
"❔ Which apps should this project have? (ex: client,backoffice,server) (separate by comma)",
);

const apps = folders.split(",").map((el, index) => ({
Expand All @@ -81,8 +81,8 @@ export default async (cmd, opts, appDir) => {
}

await select({
message: `❔ Do you want to install any development environment on ${app.name}?`,
choices: [
question: `❔ Do you want to install any development environment on ${app.name}?`,
options: [
{ name: "Vite", value: "vite" },
{ name: "None", value: "none" },
],
Expand Down Expand Up @@ -208,15 +208,15 @@ export default async (cmd, opts, appDir) => {
}

stopAnimation();
console.log(okStyle(`\r✅ ${projectName} project created`));
rl.close();
process.stdout.write(okStyle(`\r✅ ${projectName} project created\n`));
process.exit(1);
};

const handleError = (err, path) => {
console.log(errorStyle(`\n❌ ${err}`));
process.stdout.write(errorStyle(`\n❌ ${err}\n`));
if (path) {
rmSync(path, { recursive: true, force: true });
}

rl.close();
process.exit(1);
};
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import create from "./commands/create.js";
import add from "./commands/add.js";
import { dirname } from "path";
import { fileURLToPath } from "url";
//TODO: add github workflow code-quality

const rootDir = dirname(fileURLToPath(import.meta.url));

Expand Down
154 changes: 154 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { emitKeypressEvents } from "readline";
import eraseLines from "./utils/EraseLines.js";
import { existsSync, rmSync } from "fs";

Check failure on line 3 in test.js

View workflow job for this annotation

GitHub Actions / quality

This variable is unused.

Check failure on line 3 in test.js

View workflow job for this annotation

GitHub Actions / quality

This variable is unused.
import { promisifyQuestion, rl } from "./utils/PromisifyInput.js";

Check failure on line 4 in test.js

View workflow job for this annotation

GitHub Actions / quality

This variable is unused.
import { promises as fsPromises } from "fs";

Check failure on line 5 in test.js

View workflow job for this annotation

GitHub Actions / quality

This variable is unused.
import { createDirectory, runCommandOnFolder } from "./cli_commands.js";

Check failure on line 6 in test.js

View workflow job for this annotation

GitHub Actions / quality

This variable is unused.

Check failure on line 6 in test.js

View workflow job for this annotation

GitHub Actions / quality

This variable is unused.
import { startAnimation, stopAnimation, setMessage } from "./utils/TerminalLoaderIndicator.js";

Check failure on line 7 in test.js

View workflow job for this annotation

GitHub Actions / quality

This variable is unused.

Check failure on line 7 in test.js

View workflow job for this annotation

GitHub Actions / quality

This variable is unused.

Check failure on line 7 in test.js

View workflow job for this annotation

GitHub Actions / quality

This variable is unused.
import chalk from "chalk";

const input = process.stdin;
const output = process.stdout;

export const select = async opts => {
return new Promise((resolve, reject) => init(opts, resolve, reject));
};

async function init(opts, resolve, reject) {
let { question, options, pointer } = opts;

if (!pointer) pointer = "❯";

if (!question || question.length === 0) {
reject("Error: Must provide a question");
return;
}

if (!options || options.length === 0) {
reject(
"Error: Must provide the options.\nOptions can be an array of string or an array of objects with value and name as parameters.",
);
return;
}

let selectIndex = 0;
let isFirstTimeShowMenu = true;

const mappedOptions = options.map(el => ({
name: el.name ?? el,
value: el.value ?? el,
description: el.description ?? "",
}));

const createOptionMenu = () => {
const optionLength = mappedOptions.length;
if (isFirstTimeShowMenu) {
isFirstTimeShowMenu = false;
} else {
eraseLines(optionLength);
}
const padding = getPadding(20);

for (let i = 0; i < optionLength; i++) {
const selectedOption =
i === selectIndex
? `${chalk.bold.green(pointer)} ${chalk.bold.green(mappedOptions[i].name)} ${
chalk.dim(mappedOptions[i].description) ?? ""
}`
: mappedOptions[i].name;

output.write(`${padding + selectedOption}\n`);
}
};

const keyPressedHandler = (_, key) => {
const acceptedKeys = ["\r", "\x03", "\x1B", "\x1B[A", "\x1B[B"];

if (key && acceptedKeys.includes(key.sequence)) {
const optionLength = mappedOptions.length - 1;

if (key.name === "down" || key.name === "up") {
if (key.name === "down") selectIndex = selectIndex < optionLength ? selectIndex + 1 : 0;
if (key.name === "up") selectIndex = selectIndex > 0 ? selectIndex - 1 : optionLength;
createOptionMenu();
return;
}

if (key.name === "escape" || (key.name === "c" && key.ctrl)) close(mappedOptions, reject);
if (key.name === "return") enter(selectIndex, mappedOptions, resolve, question);
}
};

output.write(`${chalk.cyan.bold(question)} ${chalk.dim("(Use up and down arrows to navigate)")}\n`);

emitKeypressEvents(input);

input.setRawMode(true);
input.resume();
hideCursor();
input.on("keypress", keyPressedHandler);

if (selectIndex >= 0) {
createOptionMenu();
}

const enter = (selectIndex, options, resolve, question) => {
eraseLines(options.length + 2);
output.write(`${chalk.cyan.bold(question)} ${chalk.yellow(options[selectIndex].name)}\n`);
input.removeListener("keypress", keyPressedHandler);
input.setRawMode(false);
input.pause();
showCursor();
resolve(options[selectIndex].value);
};

const close = (options, reject) => {
eraseLines(options.length);
input.removeListener("keypress", keyPressedHandler);
input.setRawMode(false);
input.pause();
showCursor();
reject("Operation canceled");
};
}

const getPadding = (num = 10) => {
let text = " ";
for (let i = 0; i < num.length; i++) {
text += " ";
}
return text;
};

const hideCursor = () => {
output.write("\u001B[?25l");
};

const showCursor = () => {
output.write("\u001B[?25h");
};

try {
await promisifyQuestion("❔ Where do you want to create the project? (leave empty for current path)");

await select({
question: "❔ Do you want to install any development environment on?",
options: [
{ name: "Vite", value: "vite", description: "asdkasldkj" },
{ name: "None", value: "none", description: "123jkasjdl" },
],
});

await promisifyQuestion("❔ Where do you want to create the project? (leave empty for current path)");

await select({
question: "❔ aasaaaaaaaaaaaaaaaaaahhhh?",
options: ["yes", "no", "maybe"],
});
await select({
question: "❔ 123?",
options: ["1", "2", "3"],
});
} catch (err) {
console.log(err);
}
21 changes: 0 additions & 21 deletions utils/AnsiEraseLines.js

This file was deleted.

7 changes: 7 additions & 0 deletions utils/EraseLines.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const eraseLines = count => {
process.stdout.cursorTo(0);
process.stdout.moveCursor(0, -count);
process.stdout.clearScreenDown();
};

export default eraseLines;
14 changes: 8 additions & 6 deletions utils/PromisifyInput.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createInterface } from "readline";
import fileSystemCompleter from "./FileSystemCompleter.js";
import chalk from "chalk";
// import ansiEraseLines from "./AnsiEraseLines.js";
import eraseLines from "./EraseLines.js";

export const rl = createInterface({
input: process.stdin,
Expand All @@ -11,15 +11,17 @@ export const rl = createInterface({

export const promisifyQuestion = question => {
return new Promise(resolve =>
rl.question(chalk.cyan.bold(question), res => {
// process.stdout.write(ansiEraseLines(1));
// process.stdout.write("\u001B[2k\u001B[2A\u001B[G");
// process.stdout.write(`${chalk.cyan.bold("aksld")} ${chalk.yellow(res)}`);
rl.question(chalk.cyan.bold(`${question}\n`), res => {
eraseLines(2);
console.log(`${chalk.cyan.bold(question)} ${chalk.yellow(res)}`);
resolve(res);
}),
);
};

//writes to process.stdout in Node.js are sometimes asynchronous and may occur over multiple ticks of the Node.js event loop.
//Calling process.exit(), however, forces the process to exit before those additional writes to stdout can be performed.
//This is bad because we have many stdout in the application and sometimes the desire code is not executed when SIGINT
rl.on("close", () => {
process.exit(0);
// process.exit(0);
});
Loading

0 comments on commit 4cd2a34

Please sign in to comment.