From ec595650fd026b11a4c8748b5e4be67b8c163fb6 Mon Sep 17 00:00:00 2001 From: Daniel Kluge Date: Thu, 16 Dec 2021 00:21:14 +0100 Subject: [PATCH] Command framework --- components/REPL/REPLHistory.tsx | 10 +++ components/REPL/REPLInput.tsx | 13 +++- components/REPL/index.tsx | 12 +++- lib/commands.ts | 7 --- lib/commands/definitions.tsx | 85 ++++++++++++++++++++++++++ lib/commands/index.ts | 31 ++++++++++ lib/commands/types.tsx | 19 ++++++ styles/REPL/REPLHistory.module.css | 10 +++ styles/{ => REPL}/REPLInput.module.css | 1 + styles/globals.css | 10 ++- 10 files changed, 183 insertions(+), 15 deletions(-) create mode 100644 components/REPL/REPLHistory.tsx delete mode 100644 lib/commands.ts create mode 100644 lib/commands/definitions.tsx create mode 100644 lib/commands/index.ts create mode 100644 lib/commands/types.tsx create mode 100644 styles/REPL/REPLHistory.module.css rename styles/{ => REPL}/REPLInput.module.css (94%) diff --git a/components/REPL/REPLHistory.tsx b/components/REPL/REPLHistory.tsx new file mode 100644 index 0000000..894f1ec --- /dev/null +++ b/components/REPL/REPLHistory.tsx @@ -0,0 +1,10 @@ +import { NextPage } from "next"; +import styles from "../../styles/REPL/REPLHistory.module.css"; + +const REPLHistory: NextPage<{history: string[]}> = ({history}) => { + return
+ { history.map((value, idx) =>
{value === "" ? "\u00A0" : value}
) } +
; +}; + +export default REPLHistory; \ No newline at end of file diff --git a/components/REPL/REPLInput.tsx b/components/REPL/REPLInput.tsx index 23bd174..d8c2c27 100644 --- a/components/REPL/REPLInput.tsx +++ b/components/REPL/REPLInput.tsx @@ -1,7 +1,7 @@ import type { NextPage } from "next"; import React from "react"; -import { commandCompletion } from "../../lib/commands"; -import styles from "../../styles/REPLInput.module.css"; +import { commandCompletion, executeCommand } from "../../lib/commands"; +import styles from "../../styles/REPL/REPLInput.module.css"; const REPLInput: NextPage<{historyCallback: CallableFunction}> = ({historyCallback}) => { const typed = React.createRef(); @@ -25,7 +25,16 @@ const REPLInput: NextPage<{historyCallback: CallableFunction}> = ({historyCallba if(typed.current) typed.current.innerHTML = currentCmd[justTabbed % currentCmd.length]; if(completion.current) completion.current.innerHTML = ""; 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; }; diff --git a/components/REPL/index.tsx b/components/REPL/index.tsx index a3bdfc8..e1a5a5b 100644 --- a/components/REPL/index.tsx +++ b/components/REPL/index.tsx @@ -1,9 +1,15 @@ -import { useCallback, useState } from "react"; +import { useState } from "react"; import REPLInput from "./REPLInput"; +import REPLHistory from "./REPLHistory"; const REPL = () => { - const [history, manipulateHistory] = useState([]); - return ; + const [history, manipulateHistory] = useState([]); + const onCommandExecuted = (result: string[]) => manipulateHistory(result.reverse().concat(history).slice(0, 1000)); + + return (<> + + + ); }; export default REPL; \ No newline at end of file diff --git a/lib/commands.ts b/lib/commands.ts deleted file mode 100644 index 0006724..0000000 --- a/lib/commands.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/lib/commands/definitions.tsx b/lib/commands/definitions.tsx new file mode 100644 index 0000000..f75c140 --- /dev/null +++ b/lib/commands/definitions.tsx @@ -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]; \ No newline at end of file diff --git a/lib/commands/index.ts b/lib/commands/index.ts new file mode 100644 index 0000000..462e68e --- /dev/null +++ b/lib/commands/index.ts @@ -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."]; +} \ No newline at end of file diff --git a/lib/commands/types.tsx b/lib/commands/types.tsx new file mode 100644 index 0000000..c515cd7 --- /dev/null +++ b/lib/commands/types.tsx @@ -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[]; +} diff --git a/styles/REPL/REPLHistory.module.css b/styles/REPL/REPLHistory.module.css new file mode 100644 index 0000000..397458e --- /dev/null +++ b/styles/REPL/REPLHistory.module.css @@ -0,0 +1,10 @@ +.container { + height: 70vh; + overflow: auto; + display: flex; + flex-direction: column-reverse; +} + +.line { + border-bottom: 1px solid #000; +} \ No newline at end of file diff --git a/styles/REPLInput.module.css b/styles/REPL/REPLInput.module.css similarity index 94% rename from styles/REPLInput.module.css rename to styles/REPL/REPLInput.module.css index 8da6aef..f2e024e 100644 --- a/styles/REPLInput.module.css +++ b/styles/REPL/REPLInput.module.css @@ -7,6 +7,7 @@ .wrapper { position: relative; + border: 2px solid #000; } .in, .in:focus { diff --git a/styles/globals.css b/styles/globals.css index f1bd48a..22d9b4d 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -1,4 +1,8 @@ @font-face { - font-family: CascadiaCode; - src: url(fonts/CascadiaCode.woff2); - } \ No newline at end of file + font-family: CascadiaCode; + src: url(fonts/CascadiaCode.woff2); +} + +* { + box-sizing: border-box; +} \ No newline at end of file