diff --git a/package.json b/package.json new file mode 100644 index 0000000..3d2556a --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "jshell", + "version": "1.0.0", + "description": "https://git.true1ann.me/true1ann/jshell/", + "main": "src/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "true1ann", + "license": "MIT", + "packageManager": "pnpm@10.6.1", + "type": "module", + "dependencies": { + "readline-sync": "^1.4.10" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..476d190 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,23 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + readline-sync: + specifier: ^1.4.10 + version: 1.4.10 + +packages: + + readline-sync@1.4.10: + resolution: {integrity: sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==} + engines: {node: '>= 0.8.0'} + +snapshots: + + readline-sync@1.4.10: {} diff --git a/src/builtin_commands/envtest.js b/src/builtin_commands/envtest.js new file mode 100644 index 0000000..dea47af --- /dev/null +++ b/src/builtin_commands/envtest.js @@ -0,0 +1,3 @@ +export default function(shctx) { + console.log(shctx.fqa.path.parsed ?? 'Path isnt parsed or Context is not accessible'); +}; diff --git a/src/builtin_commands/nuke.js b/src/builtin_commands/nuke.js new file mode 100644 index 0000000..83132d5 --- /dev/null +++ b/src/builtin_commands/nuke.js @@ -0,0 +1,3 @@ +export default function() { + process.exit(0) +}; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..c3e1b5d --- /dev/null +++ b/src/index.js @@ -0,0 +1,165 @@ +import htmlc from './lib/htmlColors.js'; +import printc from './lib/printc.js'; +import readline from 'readline'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import readlineSync from 'readline-sync'; + +let __temp; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +let fqa = { + home: process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE, + path: { + raw: process.env.PATH, + parsed: process.env.PATH ? process.env.PATH.split(':') : [] + }, + colors: {} +}; + +__temp = { + term: { + warn: 3, + error: 1, + text: 4, + prompt: 15 + }, + html: { + warn: htmlc['Yellow'], + error: htmlc['Red'], + text: htmlc['White'], + prompt: htmlc['Gray'], + }, + none: { + warn: -1, + error: -1, + text: -1, + prompt: -1, + }, +}; + +const configPath = path.join(fqa.home, '.config', 'jshell', 'config.json'); +const historyFile = process.env.JSHELL_HISTORY || `${fqa.home}/.history`; + +if (!fs.existsSync(configPath)) { + process.stdout.write(printc('FF0000', `No config file was found in ${fqa.home}/.config/jshell/config.json.` + '\n')); + process.stdout.write('Do you want to generate a standart config now? (Y/n)' + '\n'); + + const userResponse = readlineSync.question('> '); + + if (userResponse.toLowerCase() !== 'y' && userResponse !== '') { + process.stdout.write('JSH cannot continue without a config file.' + '\n'); + process.stdout.write(printc('FF0000', 'JSH Fatal. exitting')); + process.exit(1); + } + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + const defaultConfig = { + colorMode: 'term', + prompt: 'jsh> ', + exitPhrase: 'exit', + customCommandPrefix: '.' + }; + fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2)); +} + +const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); +if (!(config.colorMode in __temp)) { + process.stdout.write(printc('FFFFFFF', 'Invalid colorMode in config. (term, html, none)')); + process.stdout.write(printc('FF0000', 'JSH Fatal. exitting')) + process.exit(1); +} + +fqa.colors = __temp[config.colorMode]; +__temp = undefined; + +const shctx = { + fqa: fqa, + config: config +} + +readline.emitKeypressEvents(process.stdin); +if (process.stdin.isTTY) process.stdin.setRawMode(true); +let shv = { + line: '', + chars: [], +}; + +process.stdin.on('keypress', async (char, key) => { + if (key.sequence === '\u0003') { + process.stdout.write(config.exitPhrase || 'exit'); + process.exit(); + } + if (key.name === 'return') { + process.stdout.write('\n'); + if (shv.line == '' || shv.line.startsWith('//')) { + // skip + } else if (shv.line.startsWith('./')) { + // LOCAL command + process.stdout.write(printc(fqa.colors.warn, 'Executing as an LOCAL command' + '\n')); + } else if (shv.line.startsWith(config.customCommandPrefix || '.')) { + process.stdout.write(printc(fqa.colors.warn, 'Executing as an JSH command') + '\n'); + // JSH command + const commandPath = path.join(__dirname, 'builtin_commands', `${shv.line.slice(config.customCommandPrefix.length)}`); + + let commandFile = commandPath; + if (!fs.existsSync(commandFile)) { + commandFile = `${commandPath}.js`; + if (!fs.existsSync(commandFile)) { + commandFile = `${commandPath}.ts`; + } + } + + if (fs.existsSync(commandFile)) { + await import(commandFile).then(async command => { + if (typeof command.default === 'function') { + await command.default(shctx); + } else { + process.stdout.write(printc(fqa.colors.error, 'Loaded module is not a function.' + '\n')); + } + }).catch(err => { + process.stdout.write(printc(fqa.colors.error, 'Failed to execute JSH command:' + '\n')); + console.error(err); + }); + } else { + process.stdout.write(printc(fqa.colors.error, 'No such JSH command.' + '\n')); + } + } else { + // UNIX command + process.stdout.write(printc(fqa.colors.warn, 'Executing as an UNIX command' + '\n')); + const command = shv.line.split(' ')[0] + let found = false; + for (const dir of fqa.path.parsed) { + const commandFile = path.join(dir, command); + if (fs.existsSync(commandFile) && fs.statSync(commandFile).isFile()) { + process.stdout.write(printc(fqa.colors.text, `Found executable: ${commandFile}` + '\n')); + found = true; + break; + } + } + if (!found) { + process.stdout.write(printc(fqa.colors.error, 'No such command.') + '\n'); + } + const args = shv.line.split(' ').slice(1); + process.stdout.write(printc(fqa.colors.text, `Args: ${JSON.stringify(args)}`) + '\n'); + } + process.stdout.write(printc(fqa.colors.prompt, config.prompt)); + shv.line = ''; + shv.chars = []; + } else if (key.name === 'backspace') { + if (shv.line.length > 0) { + shv.line = shv.line.slice(0, -1); + shv.chars.pop(); + process.stdout.clearLine(); + process.stdout.cursorTo(0); + process.stdout.write(printc(fqa.colors.prompt, config.prompt) + printc(fqa.colors.text, shv.chars.map(c => c.render).join(''))); + } + } else { + shv.line += char; + const r = printc(fqa.colors.text, char); + shv.chars.push({ render: r, rawKey: key, rawChar: char}); + process.stdout.write(r); + } +}); +process.stdout.write(printc(fqa.colors.prompt, config.prompt)); \ No newline at end of file diff --git a/src/lib/htmlColors.js b/src/lib/htmlColors.js new file mode 100644 index 0000000..ef3019b --- /dev/null +++ b/src/lib/htmlColors.js @@ -0,0 +1,147 @@ +// https://www.w3schools.com/colors/colors_names.asp + +const htmlc = { + "AliceBlue": "F0F8FF", + "AntiqueWhite": "FAEBD7", + "Aqua": "00FFFF", + "Aquamarine": "7FFFD4", + "Azure": "F0FFFF", + "Beige": "F5F5DC", + "Bisque": "FFE4C4", + "Black": "000000", + "BlanchedAlmond": "FFEBCD", + "Blue": "0000FF", + "BlueViolet": "8A2BE2", + "Brown": "A52A2A", + "BurlyWood": "DEB887", + "CadetBlue": "5F9EA0", + "Chartreuse": "7FFF00", + "Chocolate": "D2691E", + "Coral": "FF7F50", + "CornflowerBlue": "6495ED", + "Cornsilk": "FFF8DC", + "Crimson": "DC143C", + "Cyan": "00FFFF", + "DarkBlue": "00008B", + "DarkCyan": "008B8B", + "DarkGoldenRod": "B8860B", + "DarkGray": "A9A9A9", + "DarkGreen": "006400", + "DarkKhaki": "BDB76B", + "DarkMagenta": "8B008B", + "DarkOliveGreen": "556B2F", + "DarkOrange": "FF8C00", + "DarkOrchid": "9932CC", + "DarkRed": "8B0000", + "DarkSalmon": "E9967A", + "DarkSeaGreen": "8FBC8F", + "DarkSlateBlue": "483D8B", + "DarkSlateGray": "2F4F4F", + "DarkTurquoise": "00CED1", + "DarkViolet": "9400D3", + "DeepPink": "FF1493", + "DeepSkyBlue": "00BFFF", + "DimGray": "696969", + "DodgerBlue": "1E90FF", + "FireBrick": "B22222", + "FloralWhite": "FFFAF0", + "ForestGreen": "228B22", + "Fuchsia": "FF00FF", + "Gainsboro": "DCDCDC", + "GhostWhite": "F8F8FF", + "Gold": "FFD700", + "GoldenRod": "DAA520", + "Gray": "808080", + "Green": "008000", + "GreenYellow": "ADFF2F", + "HoneyDew": "F0FFF0", + "HotPink": "FF69B4", + "IndianRed": "CD5C5C", + "Indigo": "4B0082", + "Ivory": "FFFFF0", + "Khaki": "F0E68C", + "Lavender": "E6E6FA", + "LavenderBlush": "FFF0F5", + "LawnGreen": "7CFC00", + "LemonChiffon": "FFFACD", + "LightBlue": "ADD8E6", + "LightCoral": "F08080", + "LightCyan": "E0FFFF", + "LightGoldenRodYellow": "FAFAD2", + "LightGray": "D3D3D3", + "LightGreen": "90EE90", + "LightPink": "FFB6C1", + "LightSalmon": "FFA07A", + "LightSeaGreen": "20B2AA", + "LightSkyBlue": "87CEFA", + "LightSlateGray": "778899", + "LightSteelBlue": "B0C4DE", + "LightYellow": "FFFFE0", + "Lime": "00FF00", + "LimeGreen": "32CD32", + "Linen": "FAF0E6", + "Magenta": "FF00FF", + "Maroon": "800000", + "MediumAquaMarine": "66CDAA", + "MediumBlue": "0000CD", + "MediumOrchid": "BA55D3", + "MediumPurple": "9370DB", + "MediumSeaGreen": "3CB371", + "MediumSlateBlue": "7B68EE", + "MediumSpringGreen": "00FA9A", + "MediumTurquoise": "48D1CC", + "MediumVioletRed": "C71585", + "MidnightBlue": "191970", + "MintCream": "F5FFFA", + "MistyRose": "FFE4E1", + "Moccasin": "FFE4B5", + "NavajoWhite": "FFDEAD", + "Navy": "000080", + "OldLace": "FDF5E6", + "Olive": "808000", + "OliveDrab": "6B8E23", + "Orange": "FFA500", + "OrangeRed": "FF4500", + "Orchid": "DA70D6", + "PaleGoldenRod": "EEE8AA", + "PaleGreen": "98FB98", + "PaleTurquoise": "AFEEEE", + "PaleVioletRed": "DB7093", + "PapayaWhip": "FFEFD5", + "PeachPuff": "FFDAB9", + "Peru": "CD853F", + "Pink": "FFC0CB", + "Plum": "DDA0DD", + "PowderBlue": "B0E0E6", + "Purple": "800080", + "RebeccaPurple": "663399", + "Red": "FF0000", + "RosyBrown": "BC8F8F", + "RoyalBlue": "4169E1", + "SaddleBrown": "8B4513", + "Salmon": "FA8072", + "SandyBrown": "F4A460", + "SeaGreen": "2E8B57", + "SeaShell": "FFF5EE", + "Sienna": "A0522D", + "Silver": "C0C0C0", + "SkyBlue": "87CEEB", + "SlateBlue": "6A5ACD", + "SlateGray": "708090", + "Snow": "FFFAFA", + "SpringGreen": "00FF7F", + "SteelBlue": "4682B4", + "Tan": "D2B48C", + "Teal": "008080", + "Thistle": "D8BFD8", + "Tomato": "FF6347", + "Turquoise": "40E0D0", + "Violet": "EE82EE", + "Wheat": "F5DEB3", + "White": "FFFFFF", + "WhiteSmoke": "F5F5F5", + "Yellow": "FFFF00", + "YellowGreen": "9ACD32" +}; + +export default htmlc; \ No newline at end of file diff --git a/src/lib/printc.js b/src/lib/printc.js new file mode 100644 index 0000000..ad5db0e --- /dev/null +++ b/src/lib/printc.js @@ -0,0 +1,45 @@ +const colorMap = { + // standart + 0: 30, // black + 1: 31, // red + 2: 32, // green + 3: 33, // yellow + 4: 34, // blue + 5: 35, // magenta + 6: 36, // cyan + 7: 37, // white + // bright + 8: 90, // black + 9: 91, // red + 10: 92, // green + 11: 93, // yellow + 12: 94, // blue + 13: 95, // magenta + 14: 96, // cyan + 15: 97 // white +}; + +const hexToAnsi = (hex) => { + hex = hex.replace(/^#/, ''); + if (hex.length === 3) hex = hex.split('').map(c => c + c).join(''); + const r = parseInt(hex.slice(0, 2), 16); + const g = parseInt(hex.slice(2, 4), 16); + const b = parseInt(hex.slice(4, 6), 16); + return `\x1b[38;2;${r};${g};${b}m`; +}; + +const printc = (COL, TX) => { + let colorCode; + if (colorMap[COL] !== undefined) { + colorCode = colorMap[COL]; + } else if (/^#?[0-9A-Fa-f]{6}$/.test(COL)) { + return hexToAnsi(COL) + TX + '\x1b[0m'; + } else if (COL == -1) { + return TX; + } else { + throw new Error('Invalid color format'); + } + return `\x1b[${colorCode}m${TX}\x1b[0m`; +}; + +export default printc; \ No newline at end of file