Command framework

This commit is contained in:
Daniel Kluge 2021-12-16 00:21:14 +01:00
parent ced2559551
commit ec595650fd
10 changed files with 183 additions and 15 deletions

View File

@ -0,0 +1,10 @@
import { NextPage } from "next";
import styles from "../../styles/REPL/REPLHistory.module.css";
const REPLHistory: NextPage<{history: string[]}> = ({history}) => {
return <div className={styles.container}>
{ history.map((value, idx) => <div className={styles.line} key={idx}>{value === "" ? "\u00A0" : value}</div>) }
</div>;
};
export default REPLHistory;

View File

@ -1,7 +1,7 @@
import type { NextPage } from "next"; import type { NextPage } from "next";
import React from "react"; import React from "react";
import { commandCompletion } from "../../lib/commands"; import { commandCompletion, executeCommand } from "../../lib/commands";
import styles from "../../styles/REPLInput.module.css"; import styles from "../../styles/REPL/REPLInput.module.css";
const REPLInput: NextPage<{historyCallback: CallableFunction}> = ({historyCallback}) => { const REPLInput: NextPage<{historyCallback: CallableFunction}> = ({historyCallback}) => {
const typed = React.createRef<HTMLSpanElement>(); const typed = React.createRef<HTMLSpanElement>();
@ -25,7 +25,16 @@ const REPLInput: NextPage<{historyCallback: CallableFunction}> = ({historyCallba
if(typed.current) typed.current.innerHTML = currentCmd[justTabbed % currentCmd.length]; if(typed.current) typed.current.innerHTML = currentCmd[justTabbed % currentCmd.length];
if(completion.current) completion.current.innerHTML = ""; if(completion.current) completion.current.innerHTML = "";
setJustTabbed(justTabbed + 1); setJustTabbed(justTabbed + 1);
} else setJustTabbed(0);
if (e.key === "Enter") {
const result = executeCommand((e.target as HTMLInputElement).value);
(e.target as HTMLInputElement).value = "";
if(typed.current) typed.current.innerHTML = "";
if(completion.current) completion.current.innerHTML = "";
historyCallback(result);
} }
return false; return false;
}; };

View File

@ -1,9 +1,15 @@
import { useCallback, useState } from "react"; import { useState } from "react";
import REPLInput from "./REPLInput"; import REPLInput from "./REPLInput";
import REPLHistory from "./REPLHistory";
const REPL = () => { const REPL = () => {
const [history, manipulateHistory] = useState([]); const [history, manipulateHistory] = useState<string[]>([]);
return <REPLInput historyCallback={manipulateHistory} />; const onCommandExecuted = (result: string[]) => manipulateHistory(result.reverse().concat(history).slice(0, 1000));
return (<>
<REPLHistory history={history} />
<REPLInput historyCallback={onCommandExecuted} />
</>);
}; };
export default REPL; export default REPL;

View File

@ -1,7 +0,0 @@
const commandList = ["about", "navigate", "external", "help", "ed", "nano"];
export function commandCompletion(input: string): string[] {
if (input === "") return [];
const candidates = commandList.filter((cmd) => cmd.substring(0, input.length) === input);
return candidates;
}

View File

@ -0,0 +1,85 @@
import type { Command } from "./types";
function illegalUse(raw: string, cmd: Command): string[] {
return [
"Syntax error!",
`Cannot parse "${raw}"`,
""
].concat(printSyntax(cmd));
}
function checkFlags(flags: string[], cmd: Command): boolean {
if (!flags || flags === []) return true;
if (!cmd.flags) return false;
for (const flag of flags) {
const isLong = flag.substring(0,2) === "--";
const flagObj = cmd.flags.find(f => isLong ? f.long === flag.substring(2) : f.short === flag.substring(1));
if (!flagObj) return false;
}
return true;
}
function checkSubcmd(subcmds: string[], cmd: Command): boolean {
if (!subcmds || subcmds === []) return true;
if (!cmd.subcommands) return false;
for (const sc of subcmds) {
const flagObj = cmd.subcommands.find(s => s.name === sc);
if (!flagObj) return false;
}
return true;
}
export function printSyntax(cmd: Command): string[] {
let flagsOption = "";
let flagsDesc = [];
if (cmd.flags && cmd.flags.length > 0) {
flagsOption = " [";
flagsDesc.push("Flags:");
cmd.flags.forEach((flag => {
flagsOption += `-${flag.short} `;
flagsDesc.push(`\t-${flag.short}\t--${flag.long}\t${flag.desc}`);
}));
flagsOption = flagsOption.substring(0, flagsOption.length-1) + "]";
flagsDesc.push("");
}
let subcmdOption = "";
let subcmdDesc = [];
if (cmd.subcommands && cmd.subcommands.length > 0) {
subcmdOption = " [";
subcmdDesc.push("Subcommands:");
cmd.subcommands.forEach((subcmd => {
subcmdOption += `${subcmd.name}|`;
subcmdDesc.push(`\t${subcmd.name}\t${subcmd.desc}`);
}));
subcmdOption = subcmdOption.substring(0, subcmdOption.length-1) + "]";
subcmdDesc.push("");
}
return [`Usage: ${cmd.name}${flagsOption}${subcmdOption}`, ""].concat(flagsDesc).concat(subcmdDesc);
}
const about: Command = {
name: "about",
desc: "Show information about this page.",
execute: (_flags, _args, _raw) => {
return [
"Hello there wanderer.",
"So you want to know what this is about?",
"",
"Well, the answer is pretty unspectecular:",
"This site presents some stuff that the user named C0ntroller created.",
"If you wander arround you will find various projects.",
"",
"The navigation is done via this console interface.",
"Even when you open a project page you don't need your mouse - just press Esc to close it.",
"",
"I hope you enjoy your stay here!",
"If you wanted more information about the page itself, type 'project this'."
];
}
};
export const commandList = [about];

31
lib/commands/index.ts Normal file
View File

@ -0,0 +1,31 @@
import { printSyntax, commandList } from "./definitions";
//const commandList = ["about", "navigate", "external", "help", "ed", "nano"];
export function commandCompletion(input: string): string[] {
if (input === "") return [];
const candidates = commandList.filter(cmd => cmd.name.substring(0, input.length) === input).map(cmd => cmd.name);
return candidates;
}
export function executeCommand(command: string): string[] {
if (!command) return [`$ ${command}`].concat(illegalCommand(command));
const args = command.split(" ");
const cmd = commandList.find(cmd => cmd.name === args[0]);
if (!cmd) return [`$ ${command}`].concat(illegalCommand(command));
const parsed = seperateFlags(args.splice(1));
const result = parsed.flags.includes("--help") ? printSyntax(cmd) : cmd.execute(parsed.flags, parsed.subcmds, command);
return [`$ ${command}`].concat(result);
}
function seperateFlags(args: string[]): {flags: string[], subcmds: string[]} {
const flags = args.filter(arg => arg.substring(0,1) === "-");
const subcmds = args.filter(arg => arg.substring(0,1) !== "-");
return {flags, subcmds};
}
function illegalCommand(command: string): string[] {
return [`Command '${command}' not found.`, "Type 'help' for help."];
}

19
lib/commands/types.tsx Normal file
View File

@ -0,0 +1,19 @@
interface Flag {
short: string;
long: string;
desc: string;
}
interface SubCommand {
name: string;
desc: string;
}
export interface Command {
name: string;
hidden?: boolean;
desc: string;
flags?: Flag[];
subcommands?: SubCommand[];
execute: (flags: string[], args: string[], raw: string) => string[];
}

View File

@ -0,0 +1,10 @@
.container {
height: 70vh;
overflow: auto;
display: flex;
flex-direction: column-reverse;
}
.line {
border-bottom: 1px solid #000;
}

View File

@ -7,6 +7,7 @@
.wrapper { .wrapper {
position: relative; position: relative;
border: 2px solid #000;
} }
.in, .in:focus { .in, .in:focus {

View File

@ -2,3 +2,7 @@
font-family: CascadiaCode; font-family: CascadiaCode;
src: url(fonts/CascadiaCode.woff2); src: url(fonts/CascadiaCode.woff2);
} }
* {
box-sizing: border-box;
}