Skip to content

Commit

Permalink
feat: Menus! (cli.promptMenu)
Browse files Browse the repository at this point in the history
  • Loading branch information
thecodeah committed Mar 18, 2021
1 parent 7d828ed commit adc7c85
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 3 deletions.
131 changes: 130 additions & 1 deletion lib/quicli.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var $ = {
BLACK: "\x1b[30m",
RED: "\x1b[31m",
Expand Down Expand Up @@ -182,6 +218,98 @@ var cli = (function () {
}
}
};
var keyCodes = {
Exit: Buffer.from([0x03]),
Enter: Buffer.from([0x0d]),
// ArrowUp and ArrowDown are escape sequences.
ArrowUp: Buffer.from([0x1b, "[".charCodeAt(0), "A".charCodeAt(0)]),
ArrowDown: Buffer.from([0x1b, "[".charCodeAt(0), "B".charCodeAt(0)]),
};
var Menu = /** @class */ (function () {
function Menu(items) {
this.items = [];
this.cursor = 0;
if (Array.isArray(items)) {
if (items.length > 0) {
if (typeof items[0] === "string") {
// If an array of strings were provided, convert it to an array of MenuItem's
this.items = items.map(function (str, index) { return ({ text: str, value: index }); });
}
else {
var validItems = items.filter(function (item) { return "text" in item && "value" in item; });
if (validItems.length > 0) {
this.items = validItems;
}
else {
throw new Error("Menu items must either be an array of strings or an array of { text: string, value: any }");
}
}
}
}
else {
throw new Error("Menu constructor expects 'items' to be an array!");
}
}
Menu.prototype.prompt = function () {
return __awaiter(this, void 0, Promise, function () {
var _this = this;
return __generator(this, function (_a) {
this.render(false);
process.stdin.resume();
// Process the input character-by-character.
process.stdin.setRawMode(true);
return [2 /*return*/, new Promise(function (resolve) { return process.stdin.on("data", function (data) {
var keyName = Object.keys(keyCodes).find(function (name) { return data.compare(keyCodes[name]) === 0; });
if (keyName !== undefined) {
switch (keyName) {
case "ArrowUp": {
_this.cursor = Math.max(0, _this.cursor - 1);
break;
}
case "ArrowDown": {
_this.cursor = Math.min(_this.items.length - 1, _this.cursor + 1);
break;
}
case "Enter": {
process.stdin.setRawMode(false);
process.stdin.pause();
process.stdin.removeAllListeners("data"); // TODO Remove this listener specifically
resolve(_this.items[_this.cursor].value);
break;
}
case "Exit": {
process.exit(1);
}
}
_this.render();
}
}); })];
});
});
};
Menu.prototype.render = function (clear) {
if (clear === void 0) { clear = true; }
if (clear) {
process.stdout.moveCursor(0, this.items.length * -1);
process.stdout.clearScreenDown();
}
for (var i = 0; i < this.items.length; i++) {
var selected = (this.cursor === i);
cli.log((selected ? $.CYAN : $.WHITE) + " 🡪", this.items[i].text, $.DIM + $.WHITE + (selected ? "(selected)" : ""));
}
};
return Menu;
}());
function promptMenu(items) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, new Menu(items).prompt()];
case 1: return [2 /*return*/, _a.sent()];
}
});
});
}
var executed = false;
process.addListener("beforeExit", function (code) {
if (executed === false) {
Expand All @@ -191,7 +319,8 @@ var cli = (function () {
});
return {
log: log,
addCommand: addCommand
addCommand: addCommand,
promptMenu: promptMenu
};
}());
;module.exports={$,cli};
2 changes: 1 addition & 1 deletion lib/quicli.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion src/production/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ global: const cli = (function () {
include: "./log";
include: "./commands";
include: "./logic";
include: "./menu";

let executed = false;
process.addListener("beforeExit", (code) => {
Expand All @@ -15,6 +16,7 @@ global: const cli = (function () {

return {
log: log,
addCommand: addCommand
addCommand: addCommand,
promptMenu: promptMenu
};
}());
91 changes: 91 additions & 0 deletions src/production/menu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
const keyCodes = {
Exit: Buffer.from([0x03]),
Enter: Buffer.from([ 0x0d ]),
// ArrowUp and ArrowDown are escape sequences.
ArrowUp: Buffer.from([ 0x1b, "[".charCodeAt(0), "A".charCodeAt(0)]),
ArrowDown: Buffer.from([ 0x1b, "[".charCodeAt(0), "B".charCodeAt(0)]),
}

interface MenuItem {
text: string;
value: any;
}

type MenuParams = MenuItem[] | string[];

class Menu {
items: MenuItem[] = [];
cursor: number = 0;

constructor(items: MenuParams) {
if(Array.isArray(items)) {
if(items.length > 0) {
if(typeof items[0] === "string") {
// If an array of strings were provided, convert it to an array of MenuItem's
this.items = (items as string[]).map((str, index) => <MenuItem>{ text: str, value: index });
} else {
const validItems = (items as []).filter((item) => "text" in item && "value" in item)
if(validItems.length > 0) {
this.items = validItems as MenuItem[];
} else {
throw new Error("Menu items must either be an array of strings or an array of { text: string, value: any }");
}
}
}
} else {
throw new Error("Menu constructor expects 'items' to be an array!");
}
}

public async prompt(): Promise<MenuItem["value"]> {
this.render(false);

process.stdin.resume();
// Process the input character-by-character.
process.stdin.setRawMode(true);

return new Promise(resolve => process.stdin.on("data", (data) => {
const keyName = Object.keys(keyCodes).find((name) => data.compare(keyCodes[name]) === 0);
if(keyName !== undefined) {
switch(keyName) {
case "ArrowUp": {
this.cursor = Math.max(0, this.cursor - 1);
break;
}
case "ArrowDown": {
this.cursor = Math.min(this.items.length - 1, this.cursor + 1);
break;
}
case "Enter": {
process.stdin.setRawMode(false);
process.stdin.pause();
process.stdin.removeAllListeners("data"); // TODO Remove this listener specifically
resolve(this.items[this.cursor].value);
break;
}
case "Exit": {
process.exit(1);
}
}

this.render();
}
}))
}

private render(clear: boolean = true) {
if(clear) {
process.stdout.moveCursor(0, this.items.length * -1);
process.stdout.clearScreenDown();
}

for(let i = 0; i < this.items.length; i++) {
const selected = (this.cursor === i);
cli.log((selected ? $.CYAN : $.WHITE) + " 🡪", this.items[i].text, $.DIM + $.WHITE + (selected ? "(selected)" : ""));
}
}
}

async function promptMenu(items: MenuParams) {
return await new Menu(items).prompt();
}

0 comments on commit adc7c85

Please sign in to comment.