我的个人资料网站现在是一个终端

2025-05-26

我的个人资料网站现在是一个终端

年轻的时候,我总以为我的个人资料网站应该很酷,功能齐全,色彩鲜艳,动画精美,采用最新的尖端前端技术构建……
结果,年纪越大,我越喜欢简洁的终端。没有用户界面,只有文本和命令。

我上次更新我的个人资料网站时,它看起来像这样:

最后一个个人资料网站

这已经够简约了,对吧?但这还不够。现在我的个人资料网站只是一个终端:

终端配置文件

让我们看看这是如何实现的。

务实的做法

几天前,我正在构思这个想法,发现了一个很酷的库:xterms。很多应用程序都在使用它,VS Code 就是其中之一。我决定尝试一下,看看它到底有多复杂,于是我查看了文档,开始把代码添加到我的网站。正如你所见,文档写得相当不错,虽然它们确实是从 TS 文档自动生成的,但这本身就很好,因为这意味着代码本身有很好的文档记录。

在开始编码之前我设定了一些要求:

  • 我不想使用npm模块。我希望我的网站源代码简洁、精简。
  • 我想使用所有(相关)浏览器支持的JavaScript 模块
  • 终端命令应该是抽象的,以便我可以通过一些更改随意删除或添加命令

那么,如何xtermjs在不使用的情况下安装npm?解决方案很简单,我托管这些文件。我使用以下命令从 npm 包中提取了这些文件



npm v xterm dist.tarball | xargs curl | tar -xz


Enter fullscreen mode Exit fullscreen mode

并搬进package/lib/xterm.jsapp/

要使用 JavaScript 模块,我只需要将main.js文件导入为模块



<script type="module" src="/app/main.js"></script>


Enter fullscreen mode Exit fullscreen mode

终端命令

尽管不使用,typescript但假设终端命令实现以下​​接口



interface Command {
  id: string;
  description: string;
  usage: string;
  args: number;
  run: (terminal: Terminal) => Promise<void>;
}


Enter fullscreen mode Exit fullscreen mode

然后我们需要一个命令运行器来解析用户输入



interface CommandRunner {
    (term: Terminal, userInput: string) => Promise<boolean>;
}


Enter fullscreen mode Exit fullscreen mode

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');
    }
  },
};


Enter fullscreen mode Exit fullscreen mode

现在我们已经塑造了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;
}



Enter fullscreen mode Exit fullscreen mode

注意: xterm不呈现用户输入,因此我们需要在有意义时执行此操作(不是输入,不是箭头键等)

处理清屏可以实现为



if (ev.ctrlKey && ev.key === 'l') {
  term.clear();
  return;
}


Enter fullscreen mode Exit fullscreen mode

SIGINT



if (ev.ctrlKey && ev.key === 'c') {
  prompt(term);
  userInput = '';
  return;
}


Enter fullscreen mode Exit fullscreen mode

现在我们已经有了一个非常基本的工作终端,所以让我们添加一些命令

基本命令

最知名的命令是什么?我希望我的终端能够使用catlsrmexit。但请记住,这个终端实际上​​是我的个人资料网站,所以它们应该在这个上下文中有意义。所以我决定终端应该有一个文件系统,其中的文件格式如下



interface File {
  name: string;
  content: string;
}


Enter fullscreen mode Exit fullscreen mode

例子



const files = [{ name: "about.md", content: "once upon a time"}];


Enter fullscreen mode Exit fullscreen mode

考虑到这一点,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');
}


Enter fullscreen mode Exit fullscreen mode

现在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);
      }
    },
  },


Enter fullscreen mode Exit fullscreen mode

结果:

养一只猫

我觉得这应该够用了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
PREV
延迟加载图像以获得最佳性能的最佳方法
NEXT
10 个有用但难记的 CSS 选择器