From 2e033026f0c9f322e0ed3e36bc9b5d0b7fba7192 Mon Sep 17 00:00:00 2001 From: b1ek Date: Wed, 15 Mar 2023 15:58:14 +1000 Subject: [PATCH] commands api and filesystem for resume app --- react/resume/.gitignore | 1 + react/resume/package.json | 16 ++- react/resume/src/Console.js | 2 +- react/resume/src/emulator/commands/cat.js | 5 + react/resume/src/emulator/commands/index.js | 5 + react/resume/src/emulator/fs.js | 4 + react/resume/src/emulator/index.js | 5 +- react/resume/src/emulator/zsh.js | 119 ++++++++++++++------ react/resume/src/resume.css | 52 +++++++++ react/resume/src/resume.html | 13 +++ react/resume/src/resume.js | 9 +- 11 files changed, 186 insertions(+), 45 deletions(-) create mode 100644 react/resume/src/emulator/commands/cat.js create mode 100644 react/resume/src/emulator/commands/index.js create mode 100644 react/resume/src/emulator/fs.js create mode 100644 react/resume/src/resume.css create mode 100644 react/resume/src/resume.html diff --git a/react/resume/.gitignore b/react/resume/.gitignore index 4ddbee5..d4922db 100644 --- a/react/resume/.gitignore +++ b/react/resume/.gitignore @@ -1,2 +1,3 @@ .parcel-cache +parcel-bundle-reports dist \ No newline at end of file diff --git a/react/resume/package.json b/react/resume/package.json index c919fec..b2c6838 100644 --- a/react/resume/package.json +++ b/react/resume/package.json @@ -6,17 +6,29 @@ "author": "blek", "license": "MIT", "devDependencies": { + "assert": "^2.0.0", + "events": "^3.1.0", "parcel": "^2.8.3", "parcel-namer-without-hash": "^0.0.1", - "process": "^0.11.10" + "path-browserify": "^1.0.0", + "process": "^0.11.10", + "punycode": "^1.4.1", + "querystring-es3": "^0.2.1", + "stream-browserify": "^3.0.0", + "url": "^0.11.0", + "util": "^0.12.3" }, "dependencies": { + "@parcel/fs": "^2.8.3", + "memfs": "^3.4.13", "react": "^18.2.0", "react-dom": "^18.2.0", + "xterm": "^5.1.0", "xterm-for-react": "^1.0.4", "xterm-js-shell": "^1.1.3" }, "scripts": { - "start": "parcel" + "start": "parcel", + "build": "parcel build --no-source-maps" } } diff --git a/react/resume/src/Console.js b/react/resume/src/Console.js index d190641..49d5243 100644 --- a/react/resume/src/Console.js +++ b/react/resume/src/Console.js @@ -22,7 +22,7 @@ export class Console extends React.Component { } componentDidMount() { - require('./emulator')(this.terminal.current.terminal); + require('./emulator')(this.terminal.current); this.terminal.current.terminal.focus(); } } \ No newline at end of file diff --git a/react/resume/src/emulator/commands/cat.js b/react/resume/src/emulator/commands/cat.js new file mode 100644 index 0000000..72e5792 --- /dev/null +++ b/react/resume/src/emulator/commands/cat.js @@ -0,0 +1,5 @@ +const fs = require('fs'); + +module.exports = (argv, terminal) => { + terminal.writeln('hi') +} \ No newline at end of file diff --git a/react/resume/src/emulator/commands/index.js b/react/resume/src/emulator/commands/index.js new file mode 100644 index 0000000..220c4e4 --- /dev/null +++ b/react/resume/src/emulator/commands/index.js @@ -0,0 +1,5 @@ +let cmds = { + 'cat': require('./cat') +}; + +module.exports = cmds; \ No newline at end of file diff --git a/react/resume/src/emulator/fs.js b/react/resume/src/emulator/fs.js new file mode 100644 index 0000000..963123d --- /dev/null +++ b/react/resume/src/emulator/fs.js @@ -0,0 +1,4 @@ +const { fs } = require('memfs'); +fs.writeFileSync('README.md', 'uwu'); + +module.exports = fs; \ No newline at end of file diff --git a/react/resume/src/emulator/index.js b/react/resume/src/emulator/index.js index e8c5a0f..ff2c345 100644 --- a/react/resume/src/emulator/index.js +++ b/react/resume/src/emulator/index.js @@ -1,8 +1,9 @@ -module.exports = (terminal) => { +module.exports = (dom) => { + const terminal = dom.terminal; terminal.writeln('Welcome to my online resume!') terminal.writeln('Type \033[1;32mhelp\033[0m for list of commands') terminal.writeln(''); - require('./zsh')(terminal); + require('./zsh')(terminal, dom); } \ No newline at end of file diff --git a/react/resume/src/emulator/zsh.js b/react/resume/src/emulator/zsh.js index 4446b09..b50604e 100644 --- a/react/resume/src/emulator/zsh.js +++ b/react/resume/src/emulator/zsh.js @@ -1,15 +1,23 @@ import { Terminal } from 'xterm'; +import { XTerm } from 'xterm-for-react'; + +const fs = require('./fs'); +global.fs = fs; +const cmds = require('./commands'); /** * @type {Terminal} */ let terminal; +/** + * @type { XTerm } + */ +let dom; + const prompt = '\033[1;32muser@blek.codes \033[36m~ $ \033[0m'; let cmd = ''; -let history = []; -let history_pos = 0; function text_prompt() { return prompt.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''); @@ -18,17 +26,51 @@ function text_prompt() { function pr_char(char) { cmd += char; terminal.write(char); - // console.log(char.charCodeAt(0)); - if (history_pos != 0) history_pos = 0; +} + +function exec_file(f) { + + const exists = fs.existsSync(f); + if (!exists) { + terminal.write('zsh: no such file or directory: ' + f); + return; + } + + const executable = fs.accessSync(f, fs.constants.X_OK); + if (!executable) { + terminal.writeln('zsh: permission denied: ' + f); + return; + } + + terminal.writeln('This is an online resume. It is not big enough to have a script runtime.\n'); + return; } function exec_cmd() { let c = cmd; - reset_cmd(); - history.unshift(c); + reset_cmd(c); + + if (c == '') { + print_prompt(); + return; + } + + // if path + if (c.match(/^((\.|\.\.)\/|\/).+$/gm)) { + exec_file(c); + terminal.writeln(''); + print_prompt(); + return; + } + + if (cmds[c] != undefined) { + cmds[c](c.split(' '), terminal); + print_prompt(); + return; + } + terminal.writeln('zsh: command not found: ' + c); print_prompt(); - history_pos = 0; } function print_prompt() { @@ -40,28 +82,25 @@ function reprint_prompt() { print_prompt(); } -function history_up() { - if (history_pos != history.length) { - reprint_prompt(); - terminal.write(history[history_pos]); - history_pos++; - } -} - function reset_cmd() { cmd = ''; terminal.writeln(''); } -function control_char(char) { - const id = char.codePointAt(0); +/** @param { KeyboardEvent } dom */ +function control_char(id, dom) { + + const backspace = () => { + if (terminal.buffer.active.cursorX <= text_prompt().length) return; + terminal.write('\b \b'); + cmd = cmd.substring(0, cmd.length - 1); + } + switch (id) { // backspace - case 127: - if (terminal.buffer.active.cursorX <= text_prompt().length) break; - terminal.write('\b \b'); - cmd = cmd.substring(0, cmd.length - 1); + case 8: + backspace(); break; // enter @@ -70,33 +109,43 @@ function control_char(char) { break; // Ctrl+c - case 3: - terminal.write('^C'); - reset_cmd(); - print_prompt(); + case 67: + if (dom.ctrlKey) { + terminal.write('^C'); + reset_cmd(); + print_prompt(); + break; + } + + case 37: + backspace(); break; - // history up - case 27: - history_up(); - break; default: - console.log('Unknown special char: ' + id); + terminal.write('<'); + if (dom.ctrlKey) terminal.write('C'); + if (dom.altKey) terminal.write('A'); + if (dom.shiftKey) terminal.write('S'); + terminal.write(`${id}>`) break; } } function key(e) { - if (RegExp(/^\p{L}/,'u').test(e.key)) { - pr_char(e.key); + /** @type {KeyboardEvent} */ + const dom = e.domEvent; + if (dom.key.length == 1 && !(dom.ctrlKey || dom.altKey || dom.shiftKey)) { + pr_char(e.domEvent.key); } else { - control_char(e.key); + control_char(e.domEvent.keyCode, dom) } } -module.exports = (t) => { +module.exports = (t, d) => { terminal = t; + dom = d; + terminal.onKey(key); - terminal.write(prompt); + terminal.write(prompt); } \ No newline at end of file diff --git a/react/resume/src/resume.css b/react/resume/src/resume.css new file mode 100644 index 0000000..09e077e --- /dev/null +++ b/react/resume/src/resume.css @@ -0,0 +1,52 @@ +div#resume_js_app { + width: 800px; + height: 600px; + background: #212121; + border: 1px solid #e1e1e1; + border-radius: 6px; + padding: 2px; + font-family: monospace; + transition: all .15s; + box-shadow: 0 2px 1px #303030a0; + color: #e1e1e1 !important; +} + +div#resume_js_app:hover { + box-shadow: 0 2px 3px #303030; +} + +div#resume_js_app p.js_loading_indicator { + width: -moz-fit-content; + width: fit-content; + text-align: center; + margin: 0; + padding: 0; + position: relative; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +div#resume_js_app * { + color: #e1e1e1; + font-family: Source Code Pro, monospace !important; +} + +div#resume_js_app a, div#resume_js_app a:visited { + text-shadow: 0 0 #24557e30; + font-weight: 500; + text-decoration: none; + transition: all .15s; + color: #24557e !important; +} + +div#resume_js_app a:hover { + text-shadow: 0 0 4px #24557e30; +} + +div#resume_js_app table * { + text-align: left; + border: 0; +} + +/*# sourceMappingURL=resume.css.map */ diff --git a/react/resume/src/resume.html b/react/resume/src/resume.html new file mode 100644 index 0000000..8d9138c --- /dev/null +++ b/react/resume/src/resume.html @@ -0,0 +1,13 @@ + + + + resume.js + + + + + +
+ + + \ No newline at end of file diff --git a/react/resume/src/resume.js b/react/resume/src/resume.js index 3ebaa75..d648c03 100644 --- a/react/resume/src/resume.js +++ b/react/resume/src/resume.js @@ -1,6 +1,5 @@ -import { createRoot } from "react-dom/client"; -import { Console } from "./Console"; +import { Console } from './Console'; +import ReactDOM from 'react-dom/client'; -const container = document.getElementById("resume_js_app"); -const root = createRoot(container) -root.render(); \ No newline at end of file +const root = ReactDOM.createRoot(document.getElementById('resume_js_app')); +root.render() \ No newline at end of file