diff --git a/package-lock.json b/package-lock.json index 4ece3f7..aa7bfb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@zenfs/dom": "^0.2.17", "@zenfs/iso": "^0.3.0", "@zenfs/zip": "^0.5.1", + "chalk": "^5.3.0", "jquery": "^3.7.1", "utilium": "^0.7.7" }, @@ -1231,17 +1232,12 @@ } }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -1469,6 +1465,23 @@ "concat-map": "0.0.1" } }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", diff --git a/package.json b/package.json index 181ac48..36bc21c 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@zenfs/dom": "^0.2.17", "@zenfs/iso": "^0.3.0", "@zenfs/zip": "^0.5.1", + "chalk": "^5.3.0", "jquery": "^3.7.1", "utilium": "^0.7.7" }, diff --git a/src/index.ts b/src/index.ts index 944d55b..6d215cc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,11 +2,10 @@ import '@xterm/xterm/css/xterm.css'; import './styles.css'; -import { FitAddon } from '@xterm/addon-fit'; -import { Terminal } from '@xterm/xterm'; import $ from 'jquery'; import { randomHex, type Entries } from 'utilium'; import { backends, type BackendInput, type BackendInputElement } from './backends.js'; +import './shell.js'; import { instantiateTemplate } from './templates.js'; // Switching tabs @@ -58,9 +57,3 @@ $('#config .add').on('click', () => { }); $('#config .update').on('click', () => {}); - -const terminal = new Terminal(); -const fitAddon = new FitAddon(); -terminal.loadAddon(fitAddon); -terminal.open($('#terminal-container')[0]); -fitAddon.fit(); diff --git a/src/shell.ts b/src/shell.ts new file mode 100644 index 0000000..02f4388 --- /dev/null +++ b/src/shell.ts @@ -0,0 +1,153 @@ +import { FitAddon } from '@xterm/addon-fit'; +import { Terminal } from '@xterm/xterm'; +import { fs } from '@zenfs/core'; +import { cd, join, resolve, cwd, basename } from '@zenfs/core/emulation/path.js'; +import chalk from 'chalk'; +import $ from 'jquery'; + +const terminal = new Terminal(); +const fitAddon = new FitAddon(); +terminal.loadAddon(fitAddon); +terminal.open($('#terminal-container')[0]); +fitAddon.fit(); +terminal.write('\x1b[4h'); + +function ls(dir: string = '.') { + const list = fs + .readdirSync(dir) + .map(name => (fs.statSync(join(dir, name)).isDirectory() ? chalk.blue(name) : name)) + .join(' '); + terminal.writeln(list); +} + +const helpText = ` +Virtual FS shell.\r +Available commands: help, ls, cp, cd, mv, rm, cat, stat, pwd, exit/quit\r +`; + +function prompt(): string { + return `[${chalk.green(cwd == '/root' ? '~' : basename(cwd) || '/')}]$ `; +} + +function clear(): void { + terminal.write('\x1b[2K\r' + prompt()); +} +terminal.writeln(helpText); +prompt(); + +let input: string = ''; + +/** + * The index for which input is being shown + */ +let index: number = -1; + +/** + * The current, uncached input + */ +let currentInput: string = ''; + +/** + * array of previous inputs + */ +const inputs: string[] = []; + +function exec(): void { + if (input == '') { + return; + } + + const [command, ...args] = input.trim().split(' '); + try { + switch (command) { + case 'help': + terminal.writeln(helpText); + break; + case 'ls': + ls(args[0]); + break; + case 'cd': + cd(args[0] || resolve('.')); + break; + case 'cp': + fs.cpSync(args[0], args[1]); + break; + case 'mv': + fs.renameSync(args[0], args[1]); + break; + case 'rm': + fs.unlinkSync(args[0]); + break; + case 'cat': + terminal.writeln(fs.readFileSync(args[0], 'utf8')); + break; + case 'stat': + //terminal.writeln(inspect(fs.statSync(args[0]), { colors: true })); + break; + case 'pwd': + terminal.writeln(cwd); + break; + case 'exit': + case 'quit': + close(); + return; + default: + terminal.writeln('Unknown command: ' + command); + } + } catch (error) { + terminal.writeln('Error: ' + (error as Error).message); + } + + prompt(); +} + +terminal.onData(data => { + if (index == -1) { + currentInput = input; + } + const promptLength = prompt().length; + const x = terminal.buffer.active.cursorX - promptLength; + switch (data) { + case '\x1b[D': + case '\x1b[C': + terminal.write(data); + break; + case 'ArrowUp': + case '\x1b[A': + clear(); + if (index < inputs.length - 1) { + input = inputs[++index]; + } + terminal.write(input); + break; + case 'ArrowDown': + case '\x1b[B': + clear(); + if (index >= 0) { + input = index-- == 0 ? currentInput : inputs[index]; + } + terminal.write(input); + break; + case '\x7f': + if (x <= 0) { + return; + } + terminal.write('\b\x1b[P'); + input = input.slice(0, x - 1) + input.slice(x); + break; + case '\r': + if (input != inputs[0]) { + inputs.unshift(input); + } + index = -1; + input = ''; + clear(); + break; + default: + terminal.write(data); + input = input.slice(0, x) + data + input.slice(x); + } +}); + +terminal.onLineFeed(exec); +clear();