├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── public ├── cmd │ ├── cat.js │ ├── cd.js │ ├── clear.js │ ├── debug.js │ ├── echo.js │ ├── help.js │ ├── ll.js │ ├── ls.js │ ├── pwd.js │ ├── reboot.js │ ├── shutdown.js │ ├── uname.js │ ├── uptime.js │ └── version.js ├── command.js ├── config.js ├── favicon.ico ├── fsFile.js ├── fsTree.js └── index.html ├── src ├── App.vue ├── components │ ├── HistoryLines.vue │ ├── InputLine.vue │ ├── ShellContainer.vue │ └── SoftKeyBoard.vue ├── executor.js ├── main.js └── utils │ ├── checkDesktop.js │ ├── directoryHint.js │ ├── eventBus.js │ ├── fileSystem.js │ ├── getAbsolutePath.js │ ├── getHomeDir.js │ └── initUptime.js └── vue.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Haotian Zou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shell-emulator 2 | 3 | 使用 Vue3 前端框架实现的终端模拟器,具有一定的自定义能力和扩展性,可通过配置文件来个性化。 4 | 5 | 可以实现一些有趣的功能,例如作为个人主页,用终端的形式来展示自己的信息。 6 | 7 | - 展示页面:https://www.chriskim.cn/ 8 | - 灵感来源:https://axton.cc/ (似乎已经挂了) 9 | 10 | ## 配置文件 11 | 12 | ```js 13 | window.config = { 14 | // [用户名] 请务必保证fsTree中有该用户的家目录/home/,root用户家目录为/root。 15 | username: "defaultuser", 16 | // [主机名] 命令提示符中显示的主机名 17 | hostname: "emulator", 18 | // [提示符格式] {}表示的内容会被动态替换,不要修改这三个变量,仅修改前后的字符或顺序。 19 | prompt: "[{user}@{host} {cwd}]$ ", 20 | // [初始内容] 页面加载时显示的初始内容,支持HTML 21 | initialContent: `shell-emulator v${window.appVersion} by ChrisKimZHT\n\n`, 22 | // [开机时间上限] 超过这个时间后,会强制刷新为0,单位ms,该值决定了uptime和/procs/uptime的最大值 23 | uptimeLimitMS: 3600000 24 | } 25 | ``` 26 | 27 | ## 文件系统 28 | 29 | 文件系统实现在 `fsTree.js` 和 `fsFile.js` 中,分别代表文件系统的树结构和储存的文件。 30 | 31 | #### 文件 32 | 33 | 为了灵活性,所有文件均使用函数来进行定义,文件内容为函数返回的字符串。因此文件可以是动态的,如果只需要静态文件可以直接 return 字符串。 34 | 35 | 例如要定义一个文件 file 内容为 foobar,则编写以下函数: 36 | 37 | ```js 38 | export function file() { return "foobar"; } 39 | ``` 40 | 41 | #### 树结构 42 | 43 | 树结构使用嵌套对象完成,如果要实现结构: 44 | 45 | ``` 46 | ├─dirA/ 47 | │ ├─fileA 48 | │ └─dirB/ 49 | └─fileB 50 | ``` 51 | 52 | 则对应的对象为: 53 | 54 | ```js 55 | import { fileA, fileB } from './fsFile.js'; 56 | 57 | window.fsTree = { 58 | "dirA": { 59 | "fileA": fileA, 60 | "dirB": {} 61 | }, 62 | "fileB": fileB 63 | }; 64 | ``` 65 | 66 | ## 添加命令 67 | 68 | > 命令实现在 `cmd` 目录,命令注册在 `command.js` 中。如果要添加一个新命令,流程如下: 69 | > 1. 在 `cmd` 目录下创建你的命令,实现命令函数,可选实现提示函数。 70 | > 2. 在 `command.js` 导入你的命令函数以及可选的提示函数。 71 | > 3. 将你的命令函数和可选的提示函数注册到 `externalCommand` 列表中。 72 | 73 | #### 传入参数 74 | 75 | 可使用 `debug` 指令查看 76 | 77 | - `cwd`: 执行指令时的工作目录字符串。 78 | - `args`: 指令参数字符串列表。 79 | - `utils`: 框架提供的工具函数,具体用法见下文。 80 | 81 | #### 指令本体 82 | 83 | ```js 84 | export default function foobar(cwd: string, args: string[], utils: object): string { 85 | return "
this is result
" ; 86 | } 87 | ``` 88 | 89 | #### 指令提示 90 | 91 | ```js 92 | export function foobarHint(cwd: string, args: string[], utils: object): string[] { 93 | return ["hint 1", "hint 2"] ; 94 | } 95 | ``` 96 | 97 | ## 工具函数 98 | 99 | #### 总览 100 | 101 | ```js 102 | const utilsEntrance = { 103 | "checkDesktop": require("./utils/checkDesktop").default, 104 | "directoryHint": require("./utils/directoryHint").default, 105 | "eventBus": require("./utils/eventBus").default, 106 | "fileSystem": require("./utils/fileSystem"), 107 | "getAbsolutePath": require("./utils/getAbsolutePath").default, 108 | "getHomeDir": require("./utils/getHomeDir").default, 109 | "initUptime": require("./utils/initUptime").default, 110 | }; 111 | ``` 112 | 113 | #### checkDesktop.js 114 | 115 | ```js 116 | export default function checkDesktop(): boolean 117 | ``` 118 | 119 | - 若是桌面端返回 `true`,移动端返回 `false` 120 | 121 | #### directoryHint.js 122 | 123 | ```js 124 | export default function directoryHint(cwd: string, cmd: string): string[] 125 | ``` 126 | 127 | - `cwd`: 当前工作目录 128 | - `cmd`: 用户输入的部分 129 | - `return`: 可能的提示列表 130 | 131 | #### eventBus.js 132 | 133 | ```js 134 | const eventBus = mitt(); 135 | export default eventBus; 136 | ``` 137 | 138 | 就是 mitt 库的 mitt 对象,具体用法见 mitt 库。框架内内置的事件名为:`enter`, `ctrl-c`, `ctrl-l`, `arrow-up`, `arrow-down`, `tab`, `touch`, `interrupt-input`, `finished-input`, `re-input`, `change-dir`. 139 | 140 | #### fileSystem.js 141 | 142 | ```js 143 | export function getFileContent(path: string): string | null 144 | ``` 145 | 146 | 获取指定路径文件的内容,返回 null 代表文件不存在。 147 | 148 | ```js 149 | export function checkDirectory(path: string): boolean 150 | ``` 151 | 152 | 检查指定路径是否为目录。 153 | 154 | ```js 155 | export function checkFile(path: string): boolean 156 | ``` 157 | 158 | 检查指定路径是否为文件。 159 | 160 | ```js 161 | export function listDirectory(path: string): string[] | null 162 | ``` 163 | 164 | 获取指定路径目录下的所有文件和目录(已排序),返回 null 代表目录不存在。 165 | 166 | ```js 167 | export function listDirectoryWithTypes(path: string): string[][] | null 168 | ``` 169 | 170 | 获取指定路径目录下的所有文件和目录(已排序且标记类型),返回 null 代表目录不存在。 171 | 172 | 列表每一项为长度为 2 的列表,第 1 项为 "f" 或 "d" 含义为文件或路径,第二项为名称。 173 | 174 | #### getAbsolutePath.js 175 | 176 | ```js 177 | export default function getAbsolutePath(cwd: string, dest: string): string 178 | ``` 179 | 180 | - `cwd`: 当前工作目录 181 | - `dest`: 目的路径(相对 / 绝对 / 缩写) 182 | - `return`: 拼接好的绝对路径 183 | 184 | #### getHomeDir.js 185 | 186 | ```js 187 | export default function getHomeDir(): string 188 | ``` 189 | 190 | 返回当前配置下的用户家目录。 191 | 192 | #### initUptime.js 193 | 194 | ```js 195 | export default function initUptime(): void 196 | ``` 197 | 198 | 初始化 Uptime: 如果 Uptime 还未设定或 Uptime 超过最大限制,则将其更新为现在。 199 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "baseUrl": "./", 6 | "moduleResolution": "node", 7 | "paths": { 8 | "@/*": [ 9 | "src/*" 10 | ] 11 | }, 12 | "lib": [ 13 | "esnext", 14 | "dom", 15 | "dom.iterable", 16 | "scripthost" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shell-emulator", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.8.3", 12 | "mitt": "^3.0.1", 13 | "path-browserify": "^1.0.1", 14 | "vue": "^3.2.13" 15 | }, 16 | "devDependencies": { 17 | "@babel/core": "^7.12.16", 18 | "@babel/eslint-parser": "^7.12.16", 19 | "@vue/cli-plugin-babel": "~5.0.0", 20 | "@vue/cli-plugin-eslint": "~5.0.0", 21 | "@vue/cli-plugin-router": "~5.0.0", 22 | "@vue/cli-plugin-vuex": "~5.0.0", 23 | "@vue/cli-service": "~5.0.0", 24 | "eslint": "^7.32.0", 25 | "eslint-plugin-vue": "^8.0.3" 26 | }, 27 | "eslintConfig": { 28 | "root": true, 29 | "env": { 30 | "node": true 31 | }, 32 | "extends": [ 33 | "plugin:vue/vue3-essential", 34 | "eslint:recommended" 35 | ], 36 | "parserOptions": { 37 | "parser": "@babel/eslint-parser" 38 | }, 39 | "rules": { 40 | "no-unused-vars": "off" 41 | } 42 | }, 43 | "browserslist": [ 44 | "> 1%", 45 | "last 2 versions", 46 | "not dead", 47 | "not ie 11" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /public/cmd/cat.js: -------------------------------------------------------------------------------- 1 | export default function cat(cwd, args, utils) { 2 | const { getAbsolutePath } = utils; 3 | const { checkDirectory, checkFile, getFileContent } = utils.fileSystem; 4 | 5 | for (const arg of args) { 6 | if (arg.startsWith("-") || arg.startsWith("--")) { 7 | return `ls: unrecognized option '${arg}'`; 8 | } 9 | } 10 | const result = []; 11 | if (args.length === 0) { 12 | args.push("."); 13 | } 14 | for (const arg of args) { 15 | const abosolutePath = getAbsolutePath(cwd, arg); 16 | if (checkDirectory(abosolutePath)) { 17 | result.push(`cat: ${abosolutePath}: Is a directory`); 18 | continue; 19 | } 20 | if (!checkFile(abosolutePath)) { 21 | result.push(`cat: ${arg}: No such file or directory`); 22 | continue; 23 | } 24 | const file = getFileContent(abosolutePath); 25 | result.push(file); 26 | } 27 | return result.join("\n"); 28 | } 29 | 30 | export function catHint(cwd, args, utils) { 31 | const { directoryHint } = utils; 32 | const arg = args[args.length - 1]; 33 | if (arg === undefined) { 34 | return []; 35 | } 36 | if (arg.startsWith("-")) { 37 | return []; 38 | } 39 | return directoryHint(cwd, arg); 40 | } -------------------------------------------------------------------------------- /public/cmd/cd.js: -------------------------------------------------------------------------------- 1 | export default function cd(cwd, cmd, utils) { 2 | const { getAbsolutePath, eventBus } = utils; 3 | const { checkDirectory } = utils.fileSystem; 4 | 5 | if (cmd.length > 1) { 6 | return "cd: too many arguments"; 7 | } 8 | const absolutePath = getAbsolutePath(cwd, cmd[0]?.trim()); 9 | if (!checkDirectory(absolutePath)) { 10 | return `cd: ${absolutePath}: No such file or directory`; 11 | } 12 | eventBus.emit("change-dir", absolutePath); 13 | return; 14 | } 15 | 16 | export function cdHint(cwd, cmd, utils) { 17 | const { directoryHint } = utils; 18 | 19 | if (cmd.length > 1) { 20 | return []; 21 | } 22 | return directoryHint(cwd, cmd[0]); 23 | } -------------------------------------------------------------------------------- /public/cmd/clear.js: -------------------------------------------------------------------------------- 1 | export default function clear(cwd, args, utils) { 2 | const { eventBus } = utils; 3 | if (args.length > 0) { 4 | return "clear: too many arguments"; 5 | } 6 | eventBus.emit("ctrl-l"); 7 | return; 8 | } -------------------------------------------------------------------------------- /public/cmd/debug.js: -------------------------------------------------------------------------------- 1 | export default function debug(cwd, args, utils) { 2 | return [ 3 | `cwd: ${cwd}`, 4 | `args: [${args.map(arg => `"${arg}"`).join(", ")}]`, 5 | `utils: [${Object.keys(utils).join(", ")}]`, 6 | ``, 7 | ].join("\n"); 8 | } -------------------------------------------------------------------------------- /public/cmd/echo.js: -------------------------------------------------------------------------------- 1 | export default function echo(cwd, args) { 2 | return args.join(" "); 3 | } -------------------------------------------------------------------------------- /public/cmd/help.js: -------------------------------------------------------------------------------- 1 | // need add command manually 2 | const commandList = [ 3 | "echo", 4 | "clear", 5 | "pwd", 6 | "cd", 7 | "ls", 8 | "cat", 9 | "version", 10 | "uname", 11 | "hello", 12 | "help", 13 | ].sort(); 14 | 15 | export default function help(cwd, args) { 16 | if (args.length > 0) { 17 | return "help: too many arguments"; 18 | } 19 | return [ 20 | "Command format: <command> [args...]", 21 | "Available commands: " + commandList.join(", "), 22 | "", 23 | "There are also some easter eggs, try to find them all!" 24 | ].join("\n"); 25 | } -------------------------------------------------------------------------------- /public/cmd/ll.js: -------------------------------------------------------------------------------- 1 | export default function ll(cwd, args, utils) { 2 | const { getAbsolutePath } = utils; 3 | const { checkDirectory, listDirectoryWithTypes } = utils.fileSystem; 4 | 5 | let isAll = false; 6 | for (const arg of args) { 7 | if (arg.startsWith("-") || arg.startsWith("--")) { 8 | if (arg === "-a") { 9 | isAll = true; 10 | continue; 11 | } 12 | return `ls: unrecognized option '${arg}'`; 13 | } 14 | } 15 | args = args.filter(arg => !arg.startsWith("-")); 16 | let error = [], result = []; 17 | let flag = false; // 是否换行分隔 18 | if (args.length === 0) { 19 | args.push("."); 20 | } 21 | for (const arg of args) { 22 | const abosolutePath = getAbsolutePath(cwd, arg); 23 | if (!checkDirectory(abosolutePath)) { 24 | error.push(`ls: cannot access '${abosolutePath}': No such file or directory`); 25 | continue; 26 | } 27 | let list = listDirectoryWithTypes(abosolutePath); 28 | if (flag) { 29 | result.push(""); 30 | } 31 | if (args.length > 1) { 32 | result.push(`${abosolutePath}:`); 33 | } 34 | if (isAll) { 35 | list.unshift(["d", "."], ["d", ".."]); 36 | } else { 37 | list = list.filter(([type, name]) => !name.startsWith(".")); 38 | } 39 | const text = []; 40 | for (const [type, name] of list) { 41 | if (type === "f") { 42 | text.push(name); 43 | } else { 44 | text.push(`${name}/`); 45 | } 46 | } 47 | result.push(text.join("\n")); 48 | flag = true; 49 | } 50 | return error.concat(result).join("\n"); 51 | } 52 | 53 | export function llHint(cwd, args, utils) { 54 | const { directoryHint } = utils; 55 | const arg = args[args.length - 1]; 56 | if (arg === undefined) { 57 | return []; 58 | } 59 | if (arg === "-") { 60 | return ["a"]; 61 | } 62 | return directoryHint(cwd, arg); 63 | } -------------------------------------------------------------------------------- /public/cmd/ls.js: -------------------------------------------------------------------------------- 1 | export default function ls(cwd, args, utils) { 2 | const { getAbsolutePath } = utils; 3 | const { checkDirectory, listDirectoryWithTypes } = utils.fileSystem; 4 | 5 | let isAll = false; 6 | for (const arg of args) { 7 | if (arg.startsWith("-") || arg.startsWith("--")) { 8 | if (arg === "-a") { 9 | isAll = true; 10 | continue; 11 | } 12 | return `ls: unrecognized option '${arg}'`; 13 | } 14 | } 15 | args = args.filter(arg => !arg.startsWith("-")); 16 | let error = [], result = []; 17 | let flag = false; // 是否换行分隔 18 | if (args.length === 0) { 19 | args.push("."); 20 | } 21 | for (const arg of args) { 22 | const abosolutePath = getAbsolutePath(cwd, arg); 23 | if (!checkDirectory(abosolutePath)) { 24 | error.push(`ls: cannot access '${abosolutePath}': No such file or directory`); 25 | continue; 26 | } 27 | let list = listDirectoryWithTypes(abosolutePath); 28 | if (flag) { 29 | result.push(""); 30 | } 31 | if (args.length > 1) { 32 | result.push(`${abosolutePath}:`); 33 | } 34 | if (isAll) { 35 | list.unshift(["d", "."], ["d", ".."]); 36 | } else { 37 | list = list.filter(([type, name]) => !name.startsWith(".")); 38 | } 39 | const text = []; 40 | for (const [type, name] of list) { 41 | if (type === "f") { 42 | text.push(name); 43 | } else { 44 | text.push(`${name}`); 45 | } 46 | } 47 | result.push(text.join("\t\t")); 48 | flag = true; 49 | } 50 | return error.concat(result).join("\n"); 51 | } 52 | 53 | export function lsHint(cwd, args, utils) { 54 | const { directoryHint } = utils; 55 | const arg = args[args.length - 1]; 56 | if (arg === undefined) { 57 | return []; 58 | } 59 | if (arg === "-") { 60 | return ["a"]; 61 | } 62 | return directoryHint(cwd, arg); 63 | } -------------------------------------------------------------------------------- /public/cmd/pwd.js: -------------------------------------------------------------------------------- 1 | export default function pwd(cwd, args) { 2 | if (args.length > 0) { 3 | return "pwd: too many arguments"; 4 | } 5 | return cwd; 6 | } -------------------------------------------------------------------------------- /public/cmd/reboot.js: -------------------------------------------------------------------------------- 1 | export default function reboot(cwd, args) { 2 | if (args.length > 0) { 3 | return "reboot: too many arguments"; 4 | } 5 | return "System has not been booted with systemd as init system (PID 1). Can't operate.\nFailed to connect to bus: Host is down"; 6 | } -------------------------------------------------------------------------------- /public/cmd/shutdown.js: -------------------------------------------------------------------------------- 1 | export default function shutdown(cwd, args) { 2 | if (args.length > 0) { 3 | return "shutdown: too many arguments"; 4 | } 5 | return "System has not been booted with systemd as init system (PID 1). Can't operate.\nFailed to connect to bus: Host is down"; 6 | } -------------------------------------------------------------------------------- /public/cmd/uname.js: -------------------------------------------------------------------------------- 1 | export default function uname(cwd, args) { 2 | let all = false; 3 | for (const arg of args) { 4 | if (arg.startsWith("-") || arg.startsWith("--")) { 5 | if (arg === "-a" || arg === "--all") { 6 | all = true; 7 | continue; 8 | } 9 | return `uname: unrecognized option '${arg}'`; 10 | } 11 | } 12 | if (!all) { 13 | return "chriskimOS"; 14 | } else { 15 | return `chriskimOS www.chriskim.cn ${window.appVersion}-generic #187-chriskim SMP Wed Dec 20 15:08:11 UTC 2023 x86_64 x86_64 x86_64 ChrisKimZHT/shell-emulator`; 16 | } 17 | } 18 | 19 | export function unameHint(cwd, args) { 20 | const arg = args[args.length - 1]; 21 | if (arg === "-") { 22 | return ["a"]; 23 | } else if (arg === "--") { 24 | return ["all"]; 25 | } else { 26 | return []; 27 | } 28 | } -------------------------------------------------------------------------------- /public/cmd/uptime.js: -------------------------------------------------------------------------------- 1 | export default function uptime(cwd, args) { 2 | if (args.length > 0) { 3 | return "uptime: too many arguments"; 4 | } 5 | const upStamp = parseInt(localStorage.getItem("uptime")); 6 | const nowDate = new Date(); 7 | const nowHour = nowDate.getHours(); 8 | const nowMinute = nowDate.getMinutes(); 9 | const nowSecond = nowDate.getSeconds(); 10 | const nowStamp = nowDate.getTime(); 11 | const upTime = nowStamp - upStamp; 12 | const upHour = Math.floor((upTime % 86400000) / 3600000); 13 | const upMinute = Math.floor((upTime % 3600000) / 60000); 14 | return ` ${String(nowHour).padStart(2, '0')}:${String(nowMinute).padStart(2, '0')}:${String(nowSecond).padStart(2, '0')} up ${upHour} hours, ${upMinute} minutes, 1 user, load average: 0.01, 0.01, 0.00` 15 | } -------------------------------------------------------------------------------- /public/cmd/version.js: -------------------------------------------------------------------------------- 1 | export default function version(cwd, args) { 2 | if (args.length > 0) { 3 | return "version: too many arguments"; 4 | } 5 | const res = 6 | "\n\n" + 7 | "=======================\n" + 8 | ` Shell Emulator v${window.appVersion}\n` + 9 | "-----------------------\n" + 10 | "Author: ChrisKimZHT\n" + 11 | "GitHub: shell-emulator\n" + 12 | "=======================\n\n"; 13 | return res; 14 | } -------------------------------------------------------------------------------- /public/command.js: -------------------------------------------------------------------------------- 1 | // 1. 首先在cmd目录下创建你的命令,实现命令函数,可选实现提示函数。 2 | 3 | // 2. 然后在这里import导入你的命令函数以及可选的提示函数。 4 | import cat, { catHint } from "./cmd/cat.js"; 5 | import cd, { cdHint } from "./cmd/cd.js"; 6 | import clear from "./cmd/clear.js"; 7 | import debug from "./cmd/debug.js"; 8 | import echo from "./cmd/echo.js"; 9 | import help from "./cmd/help.js"; 10 | import ll, { llHint } from "./cmd/ll.js"; 11 | import ls, { lsHint } from "./cmd/ls.js"; 12 | import pwd from "./cmd/pwd.js"; 13 | import reboot from "./cmd/reboot.js"; 14 | import shutdown from "./cmd/shutdown.js"; 15 | import uname, { unameHint } from "./cmd/uname.js"; 16 | import uptime from "./cmd/uptime.js"; 17 | import version from "./cmd/version.js"; 18 | 19 | // 3. 最后将你的命令函数和可选的提示函数注册到列表中。 20 | window.externalCommand = [ 21 | { name: "cat", func: cat, hint: catHint }, 22 | { name: "cd", func: cd, hint: cdHint }, 23 | { name: "clear", func: clear }, 24 | { name: "debug", func: debug }, 25 | { name: "echo", func: echo }, 26 | { name: "help", func: help }, 27 | { name: "ll", func: ll, hint: llHint }, 28 | { name: "ls", func: ls, hint: lsHint }, 29 | { name: "pwd", func: pwd }, 30 | { name: "reboot", func: reboot }, 31 | { name: "shutdown", func: shutdown }, 32 | { name: "uname", func: uname, hint: unameHint }, 33 | { name: "uptime", func: uptime }, 34 | { name: "version", func: version } 35 | ] -------------------------------------------------------------------------------- /public/config.js: -------------------------------------------------------------------------------- 1 | window.config = { 2 | // [用户名] 请务必保证fsTree中有该用户的家目录/home/,root用户家目录为/root。 3 | username: "defaultuser", 4 | // [主机名] 命令提示符中显示的主机名 5 | hostname: "emulator", 6 | // [提示符格式] {}表示的内容会被动态替换,不要修改这三个变量,仅修改前后的字符或顺序。 7 | prompt: "[{user}@{host} {cwd}]$ ", 8 | // [初始内容] 页面加载时显示的初始内容,支持HTML 9 | initialContent: `shell-emulator v${window.appVersion} by ChrisKimZHT\n\n`, 10 | // [开机时间上限] 超过这个时间后,会强制刷新为0,单位ms,该值决定了uptime和/procs/uptime的最大值 11 | uptimeLimitMS: 3600000 12 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChrisKimZHT/shell-emulator/cd734596e5294ffbe89031f44e5424bc7e147a03/public/favicon.ico -------------------------------------------------------------------------------- /public/fsFile.js: -------------------------------------------------------------------------------- 1 | export function readme() { 2 | return [ 3 | "#############################", 4 | "# Welcome to shell-emulator #", 5 | "#############################", 6 | "", 7 | "It's a simple shell emulator made with Vue.js.", 8 | "Author: ChrisKimZHT | GitHub: shell-emulator", 9 | "", 10 | ].join("\n"); 11 | } 12 | 13 | ////////////////////////////////////////////////////////////////////// 14 | 15 | export function cpuinfo() { 16 | let result = []; 17 | for (let i = 0; i < 16; i++) { 18 | result.push([ 19 | `processor : ${i}`, 20 | `vendor_id : GenuineIntel`, 21 | `cpu family : 6`, 22 | `model : 191`, 23 | `model name : 13th Gen Intel(R) Core(TM) i5-13490F`, 24 | `stepping : 2`, 25 | `microcode : 0xffffffff`, 26 | `cpu MHz : 2495.999`, 27 | `cache size : 24576 KB`, 28 | `physical id : 0`, 29 | `siblings : 16`, 30 | `core id : 0`, 31 | `cpu cores : 8`, 32 | `apicid : 0`, 33 | `initial apicid : 0`, 34 | `fpu : yes`, 35 | `fpu_exception : yes`, 36 | `cpuid level : 21`, 37 | `wp : yes`, 38 | `flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology tsc_reliable nonstop_tsc cpuid pni pclmulqdq ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch ssbd ibrs ibpb stibp ibrs_enhanced fsgsbase bmi1 avx2 smep bmi2 erms invpcid rdseed adx smap clflushopt clwb sha_ni xsaveopt xsavec xgetbv1 xsaves umip gfni vaes vpclmulqdq rdpid fsrm flush_l1d arch_capabilities`, 39 | `bugs : spectre_v1 spectre_v2 spec_store_bypass swapgs mmio_unknown retbleed eibrs_pbrsb`, 40 | `bogomips : 4991.99`, 41 | `clflush size : 64`, 42 | `cache_alignment : 64`, 43 | `address sizes : 39 bits physical, 48 bits virtual`, 44 | `power management:\n`, 45 | ].join("\n")) 46 | } 47 | return result.join("\n"); 48 | } 49 | 50 | export function meminfo() { 51 | return [ 52 | "MemTotal: 24613188 kB", 53 | "MemFree: 23579124 kB", 54 | "MemAvailable: 23747196 kB", 55 | "Buffers: 35352 kB", 56 | "Cached: 421840 kB", 57 | "SwapCached: 0 kB", 58 | "Active: 124504 kB", 59 | "Inactive: 530752 kB", 60 | "Active(anon): 2824 kB", 61 | "Inactive(anon): 198544 kB", 62 | "Active(file): 121680 kB", 63 | "Inactive(file): 332208 kB", 64 | "Unevictable: 0 kB", 65 | "Mlocked: 0 kB", 66 | "SwapTotal: 8388608 kB", 67 | "SwapFree: 8388608 kB", 68 | "Dirty: 0 kB", 69 | "Writeback: 0 kB", 70 | "AnonPages: 194848 kB", 71 | "Mapped: 163404 kB", 72 | "Shmem: 3304 kB", 73 | "KReclaimable: 35856 kB", 74 | "Slab: 97464 kB", 75 | "SReclaimable: 35856 kB", 76 | "SUnreclaim: 61608 kB", 77 | "KernelStack: 4688 kB", 78 | "PageTables: 4744 kB", 79 | "NFS_Unstable: 0 kB", 80 | "Bounce: 0 kB", 81 | "WritebackTmp: 0 kB", 82 | "CommitLimit: 20695200 kB", 83 | "Committed_AS: 931036 kB", 84 | "VmallocTotal: 34359738367 kB", 85 | "VmallocUsed: 27572 kB", 86 | "VmallocChunk: 0 kB", 87 | "Percpu: 7040 kB", 88 | "AnonHugePages: 73728 kB", 89 | "ShmemHugePages: 0 kB", 90 | "ShmemPmdMapped: 0 kB", 91 | "FileHugePages: 0 kB", 92 | "FilePmdMapped: 0 kB", 93 | "HugePages_Total: 0", 94 | "HugePages_Free: 0", 95 | "HugePages_Rsvd: 0", 96 | "HugePages_Surp: 0", 97 | "Hugepagesize: 2048 kB", 98 | "Hugetlb: 0 kB", 99 | "DirectMap4k: 82944 kB", 100 | "DirectMap2M: 7256064 kB", 101 | "DirectMap1G: 27262976 kB", 102 | ].join("\n"); 103 | } 104 | 105 | export function uptime() { 106 | const uptime = localStorage.getItem('uptime'); 107 | const now = Date.now(); 108 | const diff = now - uptime; 109 | const seconds = (diff / 1000).toFixed(2); 110 | return seconds; 111 | } 112 | 113 | export function nothing() { 114 | return "The quick brown fox jumps over the lazy dog."; 115 | } -------------------------------------------------------------------------------- /public/fsTree.js: -------------------------------------------------------------------------------- 1 | import { cpuinfo, meminfo, nothing, readme, uptime } from './fsFile.js'; 2 | 3 | window.fsTree = { 4 | "boot": { 5 | "config": nothing, 6 | "efi": { 7 | "EFI": { 8 | "BOOT": { 9 | "BOOTX64.EFI": nothing, 10 | "fbx64.efi": nothing, 11 | "mmx64.efi": nothing, 12 | }, 13 | "chriskimOS": { 14 | "BOOTX64.CSV": nothing, 15 | "grub.cfg": nothing, 16 | "grubx64.efi": nothing, 17 | "mmx64.efi": nothing, 18 | "shimx64.efi": nothing, 19 | } 20 | } 21 | }, 22 | "grub": { 23 | "fonts": { 24 | "unicode.pf2": nothing, 25 | }, 26 | "grub.cfg": nothing, 27 | "grubenv": nothing, 28 | "i386-pc": { 29 | "normal.mod": nothing, 30 | }, 31 | "locale": { 32 | "en.mo": nothing, 33 | }, 34 | "x86_64-efi": { 35 | "normal.mod": nothing, 36 | } 37 | }, 38 | "initrd.img": nothing, 39 | "vmlinuz": nothing, 40 | }, 41 | "dev": { 42 | "cpu": { 43 | "0": { "msr": nothing }, "1": { "msr": nothing }, "2": { "msr": nothing }, "3": { "msr": nothing }, 44 | "4": { "msr": nothing }, "5": { "msr": nothing }, "6": { "msr": nothing }, "7": { "msr": nothing }, 45 | "8": { "msr": nothing }, "9": { "msr": nothing }, "10": { "msr": nothing }, "11": { "msr": nothing }, 46 | "12": { "msr": nothing }, "13": { "msr": nothing }, "14": { "msr": nothing }, "15": { "msr": nothing }, 47 | }, 48 | "disk": { 49 | "by-id": { 50 | "ata-QEMU_HARDDISK_QM00001": nothing, 51 | "ata-QEMU_HARDDISK_QM00001-part1": nothing, 52 | "ata-QEMU_HARDDISK_QM00001-part2": nothing, 53 | "ata-QEMU_HARDDISK_QM00001-part3": nothing, 54 | }, 55 | "by-partuuid": { 56 | "4fcb06da-29c9-4b28-b8b0-e35c9e07abe5": nothing, 57 | "7ba2bd7b-c746-4851-a5f5-42bc393930d6": nothing, 58 | "511eceff-a7b6-46e9-b7b9-d29dbd9ff204": nothing 59 | }, 60 | "by-path": { 61 | "pci-0000:00:01.1-ata-1": nothing, 62 | "pci-0000:00:01.1-ata-1-part1": nothing, 63 | "pci-0000:00:01.1-ata-1-part2": nothing, 64 | "pci-0000:00:01.1-ata-1-part3": nothing, 65 | }, 66 | "by-uuid": { 67 | "4a26edeb-942a-4c09-bd9d-ab64b13ec54d": nothing, 68 | } 69 | }, 70 | "dma_heap": { 71 | "system": nothing, 72 | }, 73 | "dri": { 74 | "card0": nothing, 75 | "renderD128": nothing, 76 | }, 77 | "fd": { 78 | "0": nothing, "1": nothing, "2": nothing, "3": nothing 79 | }, 80 | "full": nothing, 81 | "fuse": nothing, 82 | "initctl": nothing, 83 | "loop0": nothing, "loop1": nothing, "loop2": nothing, "loop3": nothing, "loop4": nothing, "loop5": nothing, "loop6": nothing, "loop7": nothing, "loop-control": nothing, 84 | "mapper": { 85 | "control": nothing, 86 | }, 87 | "mem": nothing, 88 | "mqueue": {}, 89 | "net": { 90 | "dev": { 91 | "lo": nothing, 92 | }, 93 | "tun": nothing, 94 | }, 95 | "null": nothing, 96 | "port": nothing, 97 | "ptmx": nothing, 98 | "random": nothing, 99 | "rtc": nothing, 100 | "stderr": nothing, 101 | "stdin": nothing, 102 | "stdout": nothing, 103 | "tty": nothing, "tty0": nothing, "tty1": nothing, "tty2": nothing, "tty3": nothing, "tty4": nothing, "tty5": nothing, "tty6": nothing, "tty7": nothing, "ttyprintk": nothing, "ttyS0": nothing, "ttyS1": nothing, "ttyS2": nothing, "ttyS3": nothing, "ttyS4": nothing, "ttyS5": nothing, "ttyS6": nothing, "ttyS7": nothing, 104 | "urandom": nothing, 105 | "vcs": nothing, "vcs0": nothing, "vcs1": nothing, "vcs2": nothing, "vcs3": nothing, "vcs4": nothing, "vcs5": nothing, "vcs6": nothing, 106 | "vcsa": nothing, "vcsa0": nothing, "vcsa1": nothing, "vcsa2": nothing, "vcsa3": nothing, "vcsa4": nothing, "vcsa5": nothing, "vcsa6": nothing, 107 | "vcsu": nothing, "vcsu0": nothing, "vcsu1": nothing, "vcsu2": nothing, "vcsu3": nothing, "vcsu4": nothing, "vcsu5": nothing, "vcsu6": nothing, 108 | "vfio": { "vfio": nothing }, 109 | "vga_arbiter": nothing, 110 | "vhci": nothing, 111 | "vhost-net": nothing, 112 | "zero": nothing, 113 | "zfs": nothing, 114 | }, 115 | "etc": {}, 116 | "home": { 117 | "defaultuser": { 118 | "readme.txt": readme 119 | } 120 | }, 121 | "media": {}, 122 | "mnt": {}, 123 | "opt": {}, 124 | "proc": { 125 | "cpuinfo": cpuinfo, 126 | "meminfo": meminfo, 127 | "uptime": uptime, 128 | }, 129 | "root": {}, 130 | "run": {}, 131 | "srv": {}, 132 | "sys": { 133 | "block": {}, "bus": {}, "class": {}, "dev": {}, "devices": {}, "firmware": {}, "fs": {}, "hypervisor": {}, "kernel": {}, "module": {}, "power": {}, 134 | }, 135 | "tmp": {}, 136 | "usr": { 137 | "bin": {}, "include": {}, "lib": {}, "lib32": {}, "lib64": {}, "libexec": {}, "libx32": {}, "local": {}, "sbin": {}, "share": {}, "src": {}, 138 | }, 139 | "var": { 140 | "backups": {}, "cache": {}, "crash": {}, "lib": {}, "local": {}, "lock": {}, "log": {}, "mail": {}, "opt": {}, "run": {}, "snap": {}, "spool": {}, "tmp": {}, 141 | } 142 | }; -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 65 | 66 | 67 | 98 | -------------------------------------------------------------------------------- /src/components/HistoryLines.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/InputLine.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 171 | 172 | -------------------------------------------------------------------------------- /src/components/ShellContainer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 67 | 68 | -------------------------------------------------------------------------------- /src/components/SoftKeyBoard.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 45 | 46 | -------------------------------------------------------------------------------- /src/executor.js: -------------------------------------------------------------------------------- 1 | const commands = window.externalCommand; 2 | 3 | const utilsEntrance = { 4 | "checkDesktop": require("./utils/checkDesktop").default, 5 | "directoryHint": require("./utils/directoryHint").default, 6 | "eventBus": require("./utils/eventBus").default, 7 | "fileSystem": require("./utils/fileSystem"), 8 | "getAbsolutePath": require("./utils/getAbsolutePath").default, 9 | "getHomeDir": require("./utils/getHomeDir").default, 10 | "initUptime": require("./utils/initUptime").default, 11 | }; 12 | 13 | export default function executor(cwd, cmd) { 14 | const cmdSplit = cmd.split(" ").map(x => x.trim()).filter(x => x.length > 0); 15 | const cmdName = cmdSplit[0]; 16 | if (cmdName === null || cmdName === undefined || cmdName.length === 0) { 17 | return; 18 | } 19 | const cmdArgs = cmdSplit.slice(1); 20 | for (const command of commands) { 21 | if (command.name === cmdName) { 22 | return command.func(cwd, cmdArgs, utilsEntrance); 23 | } 24 | } 25 | return `${cmdName}: command not found`; 26 | } 27 | 28 | export function getHint(cwd, cmd) { 29 | const cmdSplit = cmd.split(" ").map(x => x.trim()).filter(x => x.length > 0); 30 | const cmdName = cmdSplit[0]; 31 | if (cmdName === null || cmdName === undefined || cmdName.length === 0) { 32 | return []; 33 | } 34 | if (cmd.endsWith(" ") || cmd.endsWith("\u00a0")) { // \u00a0 is   35 | return []; 36 | } 37 | for (const command of commands) { 38 | if (command.name === cmdName) { 39 | return command.hint?.(cwd, cmdSplit.slice(1), utilsEntrance) ?? []; 40 | } 41 | } 42 | if (cmdSplit.length > 1) { 43 | return []; 44 | } 45 | let hint = []; 46 | for (const command of commands) { 47 | if (command.name.startsWith(cmdName)) { 48 | hint.push(command.name.slice(cmdName.length)); 49 | } 50 | } 51 | hint.sort((a, b) => { 52 | if (a.length !== b.length) { 53 | return a.length - b.length; 54 | } 55 | return a.localeCompare(b); 56 | }); 57 | return hint; 58 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | 4 | createApp(App).mount("#app") 5 | -------------------------------------------------------------------------------- /src/utils/checkDesktop.js: -------------------------------------------------------------------------------- 1 | export default function checkDesktop() { 2 | const isMobile = /Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent); 3 | const isTablet = /iPad/i.test(navigator.userAgent); 4 | const isDesktop = !isMobile && !isTablet; 5 | return isDesktop; 6 | } -------------------------------------------------------------------------------- /src/utils/directoryHint.js: -------------------------------------------------------------------------------- 1 | import { listDirectory } from "../utils/fileSystem.js"; 2 | import getAbsolutePath from "@/utils/getAbsolutePath.js"; 3 | 4 | export default function directoryHint(cwd, cmd) { 5 | let absolutePath = getAbsolutePath(cwd, cmd?.trim()); 6 | if (cmd?.endsWith("/") && !absolutePath.endsWith("/")) { 7 | absolutePath += "/"; 8 | } 9 | const lastSlashIdx = absolutePath.lastIndexOf("/"); 10 | const prefix = absolutePath.slice(0, lastSlashIdx); // 前面的路径 11 | const suffix = absolutePath.slice(lastSlashIdx + 1); // 没输完的路径 12 | const dirList = listDirectory(prefix); 13 | if (dirList === null) { 14 | return []; 15 | } 16 | const result_normal = [], result_hidden = []; 17 | for (const dir of dirList) { 18 | if (dir.startsWith(suffix)) { 19 | if (dir.startsWith(".")) { 20 | result_hidden.push(dir.slice(suffix.length)); 21 | } else { 22 | result_normal.push(dir.slice(suffix.length)); 23 | } 24 | } 25 | } 26 | return result_normal.concat(result_hidden); // 保证正常文件在前 27 | } -------------------------------------------------------------------------------- /src/utils/eventBus.js: -------------------------------------------------------------------------------- 1 | import mitt from "mitt"; 2 | 3 | const eventBus = mitt(); 4 | 5 | export default eventBus; -------------------------------------------------------------------------------- /src/utils/fileSystem.js: -------------------------------------------------------------------------------- 1 | const tree = window.fsTree; 2 | 3 | export function getFileContent(path) { 4 | const pathSplit = path.split("/").map(x => x.trim()).filter(x => x.length > 0); 5 | let current = tree; 6 | for (const dir of pathSplit) { 7 | if (current[dir] === undefined) { 8 | return null; 9 | } 10 | current = current[dir]; 11 | } 12 | if (typeof current !== "function") { 13 | return null; 14 | } 15 | return current(); 16 | } 17 | 18 | export function checkDirectory(path) { 19 | const pathSplit = path.split("/").map(x => x.trim()).filter(x => x.length > 0); 20 | let current = tree; 21 | for (const dir of pathSplit) { 22 | if (current[dir] === undefined) { 23 | return false; 24 | } 25 | current = current[dir]; 26 | } 27 | if (typeof current === "function") { 28 | return false; 29 | } 30 | return true; 31 | } 32 | 33 | export function listDirectory(path) { 34 | const pathSplit = path.split("/").map(x => x.trim()).filter(x => x.length > 0); 35 | let current = tree; 36 | for (const dir of pathSplit) { 37 | if (current[dir] === undefined) { 38 | return null; 39 | } 40 | current = current[dir]; 41 | } 42 | if (typeof current === "function") { 43 | return null; 44 | } 45 | return Object.keys(current).sort(); 46 | } 47 | 48 | export function listDirectoryWithTypes(path) { 49 | const pathSplit = path.split("/").map(x => x.trim()).filter(x => x.length > 0); 50 | let current = tree; 51 | for (const dir of pathSplit) { 52 | if (current[dir] === undefined) { 53 | return null; 54 | } 55 | current = current[dir]; 56 | } 57 | if (typeof current === "function") { 58 | return null; 59 | } 60 | const result = [] 61 | for (const key of Object.keys(current).sort()) { 62 | result.push([typeof current[key] === "function" ? "f" : "d", key]); 63 | } 64 | return result; 65 | } 66 | 67 | export function checkFile(path) { 68 | const pathSplit = path.split("/").map(x => x.trim()).filter(x => x.length > 0); 69 | let current = tree; 70 | for (const dir of pathSplit) { 71 | if (current[dir] === undefined) { 72 | return false; 73 | } 74 | current = current[dir]; 75 | } 76 | if (typeof current !== "function") { 77 | return false; 78 | } 79 | return true; 80 | } -------------------------------------------------------------------------------- /src/utils/getAbsolutePath.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import getHomeDir from "./getHomeDir"; 3 | 4 | export default function getAbsolutePath(cwd, dest) { 5 | let absolutePath = ""; 6 | if (dest === undefined || dest === null || dest.length === 0) { 7 | absolutePath = getHomeDir(); 8 | } else if (dest[0] === "~") { 9 | absolutePath = path.resolve(getHomeDir() + dest.slice(1)); 10 | } else { 11 | absolutePath = path.resolve(cwd, dest); 12 | } 13 | return absolutePath; 14 | } -------------------------------------------------------------------------------- /src/utils/getHomeDir.js: -------------------------------------------------------------------------------- 1 | export default function getHomeDir() { 2 | if (window.config.username === "root") { 3 | return "/root"; 4 | } else { 5 | return "/home/" + window.config.username; 6 | } 7 | } -------------------------------------------------------------------------------- /src/utils/initUptime.js: -------------------------------------------------------------------------------- 1 | export default function initUptime() { 2 | if (localStorage.getItem("uptime") === null || localStorage.getItem("uptime") === undefined) { 3 | localStorage.setItem("uptime", Date.now()); 4 | return; 5 | } 6 | if (Date.now() - localStorage.getItem("uptime") > window.config.uptimeLimitMS) { 7 | localStorage.setItem("uptime", Date.now()); 8 | return; 9 | } 10 | } -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('@vue/cli-service') 2 | module.exports = defineConfig({ 3 | transpileDependencies: true, 4 | configureWebpack: { 5 | resolve: { 6 | fallback: { 7 | path: require.resolve("path-browserify"), 8 | } 9 | } 10 | } 11 | }) 12 | --------------------------------------------------------------------------------