我的个人资料网站现在是一个终端
年轻的时候,我总以为我的个人资料网站应该很酷,功能齐全,色彩鲜艳,动画精美,采用最新的尖端前端技术构建……
结果,年纪越大,我越喜欢简洁的终端。没有用户界面,只有文本和命令。
我上次更新我的个人资料网站时,它看起来像这样:
这已经够简约了,对吧?但这还不够。现在我的个人资料网站只是一个终端:
让我们看看这是如何实现的。
务实的做法
几天前,我正在构思这个想法,发现了一个很酷的库:xterms。很多应用程序都在使用它,VS Code 就是其中之一。我决定尝试一下,看看它到底有多复杂,于是我查看了文档,开始把代码添加到我的网站。正如你所见,文档写得相当不错,虽然它们确实是从 TS 文档自动生成的,但这本身就很好,因为这意味着代码本身有很好的文档记录。
在开始编码之前我设定了一些要求:
- 我不想使用
npm
模块。我希望我的网站源代码简洁、精简。 - 我想使用所有(相关)浏览器支持的JavaScript 模块
- 终端命令应该是抽象的,以便我可以通过一些更改随意删除或添加命令
那么,如何xtermjs
在不使用的情况下安装npm
?解决方案很简单,我托管这些文件。我使用以下命令从 npm 包中提取了这些文件
npm v xterm dist.tarball | xargs curl | tar -xz
并搬进package/lib/xterm.js
了app/
要使用 JavaScript 模块,我只需要将main.js
文件导入为模块
<script type="module" src="/app/main.js"></script>
终端命令
尽管不使用,typescript
但假设终端命令实现以下接口
interface Command {
id: string;
description: string;
usage: string;
args: number;
run: (terminal: Terminal) => Promise<void>;
}
然后我们需要一个命令运行器来解析用户输入
interface CommandRunner {
(term: Terminal, userInput: string) => Promise<boolean>;
}
false
如果未找到命令,运行器将返回。
现在我们定义一个命令:
const lsCommand = {
id: "ls",
description: 'list files',
usage: '[usage]: ls filename'
args: 0,
async run(term, args) {
for (const file of files) {
term.write(file.name + '\t\t');
}
},
};
现在我们已经塑造了command
,我们可以考虑处理用户输入。
终端基本功能
终端应支持:
-
它应该显示
prompt
-
ctrl + l
:应该清除终端 -
ctrl + c
:应该发送SIGINT
-
enter
:应该从当前用户输入运行命令
终端还应该处理常见错误:
- 未找到命令
- 带有错误参数的命令
考虑到这一点,我们就可以开始处理用户输入。
xterm
提供了一个onKey
接收处理函数的事件({ key, domEvent }) => void
,因此用户每次按下按键时,我们都会收到一个事件。这意味着我们需要跟踪用户输入,并将每个按键添加为一个字符。当用户按下按键时,enter
我们应该评估迄今为止的输入。非常简单
let userInput = '';
if (ev.keyCode == 13) {
await runCommand(term, userInput);
userInput = '';
prompt(term);
} else {
term.write(key);
userInput += key;
}
注意: xterm
不呈现用户输入,因此我们需要在有意义时执行此操作(不是输入,不是箭头键等)
处理清屏可以实现为
if (ev.ctrlKey && ev.key === 'l') {
term.clear();
return;
}
和SIGINT
if (ev.ctrlKey && ev.key === 'c') {
prompt(term);
userInput = '';
return;
}
现在我们已经有了一个非常基本的工作终端,所以让我们添加一些命令
基本命令
最知名的命令是什么?我希望我的终端能够使用cat
、ls
、rm
、exit
。但请记住,这个终端实际上是我的个人资料网站,所以它们应该在这个上下文中有意义。所以我决定终端应该有一个文件系统,其中的文件格式如下
interface File {
name: string;
content: string;
}
例子
const files = [{ name: "about.md", content: "once upon a time"}];
考虑到这一点,cat
将打印文件内容,ls
将打印每个文件的名称,rm
并将从数组中删除该文件。
对于exit
命令,我们可以从 javascript 关闭窗口:window.close()
。
更进一步
我决定创建一个文件,blog.md
其中包含我最近的 5 篇博文。为了获取这些信息,我使用了Hugo为我的博客
生成的 RSS feed xml 文件。我需要做的就是获取这个文件,解析文档,然后获取每篇博文的标题和链接:xml
export async function fecthLastPosts() {
const res = await fetch('/blog/index.xml');
const text = await res.text();
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(text,"text/xml");
const posts = xmlDoc.getElementsByTagName('item');
const lastPosts = [];
for (let i = 0; i < 5; i++) {
const title = posts[i].getElementsByTagName('title')[0].childNodes[0].nodeValue;
const link = posts[i].getElementsByTagName('link')[0].childNodes[0].nodeValue;
lastPosts.push(title + `\r\n${link}\r\n`);
}
files[0].content = lastPosts.join('\n');
}
现在cat blog.md
打印我最近的 5 篇帖子,而且由于插件web link
的存在,xterm
每个链接都可以点击。好棒啊。
不过,干嘛就到此为止呢?每个hackerman
终端都应该有相应的whoami
命令。所以这个命令只会打印我自己的信息。
另外,一些很酷的 Web 应用包含猫咪照片,所以我决定编写一个randc
命令,可以打开一张随机的猫咪照片。
为此,我找到了这个很棒的REST API
{
id: "randc",
description: 'get a random cat photo',
args: 0,
async run(term, args) {
term.writeln('getting a cato...');
const res = await fetch('https://cataas.com/cat?json=true');
if (!res.ok) {
term.writeln(`[error] no catos today :( -- ${res.statusText}`));
} else {
const { url } = await res.json();
term.writeln(colorize(TermColors.Green, 'opening cato...'));
await sleep(1000);
window.open('https://cataas.com' + url);
}
},
},
结果:
我觉得这应该够用了profile terminal
。我对它的简洁性和我实现的命令非常满意。
以后我可能会添加更多命令,也可能会实现streams
,纯粹为了好玩。
你想在配置文件终端中添加什么命令?
快来试试吧:https://protiumx.dev
更新:
我重构了项目结构,以提高可读性并使其更通用。
它还会从本地存储加载您的命令历史记录。所有更改可在此处查看:https://github.com/protiumx/protiumx.github.io/pull/1
更新 2:
rm
支持全局模式
更新 3:
- 添加的
man
命令 - 添加的
uname
命令
其他文章:
👽
文章来源:https://dev.to/protium/my-profile-website-is-now-a-terminal-2j57