DEV Community

Cover image for My profile website is now a terminal
protium
protium

Posted on • Updated on

My profile website is now a terminal

When I was younger I used to think that my profile website would be a really cool, fully featured website, with shiny colors and animations; built with the latest cutting edge frontend technology...
Turns out that the older I get, the more I prefer a simple terminal. No UI, just text and commands.

The last time I updated my profile website, it looked like this:

last profle website

It was already pretty minimalistic, right? But not enough. Now my profile website is just a terminal:

terminal profile

Let's see how this was possible.

A pragmatic approach

A few days ago I was shaping this idea on my head and found this cool library: xterms. It's been used by a lot of apps, VS Code being among them. I decided to give it a try to see how complex could it be, so I headed to the docs and started adding the code to my website. As you can see the docs are pretty good, they are surely autogenerated from TS docs but this is good because it means the code itself is well documented.

Before starting coding I set a few requirements:

  • I don't want to use npm modules. I want my website source to be simple and minimal
  • I want to make use of javascript modules which are supported by all (relevant) browsers
  • The terminal commands should be abstract to allow me to remove or add commands at will with a few changes

Then, how do I install xtermjs without using npm? The solution is simple, I host the files. I extracted the files from the npm packages with this command

npm v xterm dist.tarball | xargs curl | tar -xz
Enter fullscreen mode Exit fullscreen mode

and moved package/lib/xterm.js into app/

To use javascript modules, I just needed to import the main.js file as module

<script type="module" src="/app/main.js"></script>
Enter fullscreen mode Exit fullscreen mode

Terminal Commands

Although not using typescript let's say that the terminal commands implement the following interface

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

Then we need a command runner that will parse the user input

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

The runner will return false if a command was not found.
Let's now define 1 command:

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

Now that we shaped the command, we can think of handling user input.

Terminal basic functionality

The terminal should support:

  • It should show a prompt

  • ctrl + l: should clear the terminal

  • ctrl + c: should send a SIGINT

  • enter: should run a command from the current user input

The terminal should also handle common errors:

  • command not found
  • command with wrong arguments

With this in mind we can start handling the user input.

xterm provides a onKey event which receives a handler function ({ key, domEvent }) => void, so we receive an event per each key press done by the user. This means that we need to track the user input and add each key as a char. When the user presses enter we should evaluate the input we have so far. Pretty straigt forward

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

NOTE: xterm doesn't render the user input, so we need to do it when it makes sense (not enter, not an arrow key, etc)

Handling the clear-screen can be implemented as

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

and the SIGINT

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

At this point we have a pretty basic working terminal, so let's add some more commands

Basic commands

What are the most known commands? For my terminal I want to be able to use cat, ls, rm, exit. But remember that this terminal is actually my profile website, so they should make sense in that context. So I decided the terminal should have a file system, where files are shaped like

interface File {
  name: string;
  content: string;
}
Enter fullscreen mode Exit fullscreen mode

Example

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

With this in mind, cat will print the file content, ls will print each file's name and rm will delete the file from the array.

For the exit command we can just close the window from javascript: window.close().

hacker man

Going further

I have decided that I wanted to have a file named blog.md which should contain my last 5 posts.
To fetch this info, I used the RSS feed xml file generated by hugo for my blog. All I need to do is to fetch the file, parse the xml document and get the title and links of each post:

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

Now cat blog.md prints my last 5 posts, and thanks to the web link addon of xterm each link is clickeable. Noice.
But why stopping here? Every hackerman terminal should have a whoami command. So this command will just print information about my self.

Also, cool web apps contain photos of cats, so I decided to write a randc command what will open a random photo of a cat.
For this I found this amazing 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

The result:

get a cat

I think this should do it for a profile terminal. I'm very satisfied with the simplicity of it and the commands I have implemented.
I'll problaly add more commands in the future and also implement streams , just for fun.

What command would you add to your profile terminal?
Go have some fun with it: https://protiumx.dev

Update:

I have refactored the project structure to improve readability and make it more generic.
It also loads your command history from the local storage. All the changes can be seen here: https://github.com/protiumx/protiumx.github.io/pull/1

Update 2:

  • rm supports glob pattern

Update 3:

  • added man command
  • added uname command

Other articles:

👽

Oldest comments (59)

Collapse
 
andrewbaisden profile image
Andrew Baisden

Cool concept.

Collapse
 
protium profile image
protium

It looks nice!

Collapse
 
snikhill profile image
Nikkhiel Seath

To borrow and modify a lyric: "The older I get the more I realise" that it is the simple stuff and not complex ui that makes a portfolio success.

Collapse
 
ben profile image
Ben Halpern

Very good execution

Collapse
 
protium profile image
protium

oh thanks Ben!

Collapse
 
vulcanwm profile image
Medea

This is really cool!

Collapse
 
protium profile image
protium

thanks! ❤️

Collapse
 
andresbecker profile image
Andres Becker

This is great inspiration for my own site

Collapse
 
protium profile image
protium

Really glad that’s the case!

Collapse
 
snelson1 profile image
Sophia Nelson

Excellent read

Collapse
 
pbnj profile image
Peter Benjamin (they/them)

What command would you add to your profile terminal?

First thing I tried was vim .

Then I tried sudo apt update && sudo apt install -y vim.

Therefore, I am requesting these features so I can do remote development on your website 😅

Collapse
 
protium profile image
protium

well, it is possible to use the cloud9 editor with vim bindings 🤔
ace.c9.io/build/kitchen-sink.html

Collapse
 
rajeshroyal profile image
Rajesh Royal

🤣

Collapse
 
luiz0x29a profile image
Real AI

sudo emerge vim

Collapse
 
zodman profile image
Andres 🐍 in 🇨🇦

good effort!

Collapse
 
juniordevforlife profile image
Jason F

Where's the neofetch command? 😁

Kidding, sorta

This is awesome! It's projects like this that keep the web fun!

Collapse
 
protium profile image
protium

That could be fun! I just need to figure out what information would make sense to show haha

Collapse
 
rajeshroyal profile image
Rajesh Royal

tip: - if you are using ad blockers you may not see the terminal text. You may need to open it in incognito.

Collapse
 
christiankozalla profile image
Christian Kozalla

I experienced this differently. I'm using brave both on my phone and laptop. On my laptop everything was working fine, but on mobile I could see my own text input and command output

Collapse
 
protium profile image
protium

which add blocker? I didn’t experience the same

Collapse
 
rajeshroyal profile image
Rajesh Royal

Ghostry, Popup blocker, YouTube ad blocker, and Ad Blocker [hand sign logo] these three.

Thread Thread
 
protium profile image
protium

Interesting. I only use uBlock. I guess the popup blocker will block the command randc or open since it opens a new tab/window

Collapse
 
sakitkepala profile image
Andika Priyotama

Really cool! I feel inspired to get one 😬

Collapse
 
vardhiro profile image
Dhirodatto Biswas

Nice idea though but not working in my phone 😅😓.

Collapse
 
protium profile image
protium

oh damn, which browser?

Collapse
 
vardhiro profile image
Dhirodatto Biswas

Chrome.....

Collapse
 
oatw profile image
oatw • Edited

Nice!

What command would you add to your profile terminal?

rm -rf /*
Enter fullscreen mode Exit fullscreen mode

😅

Collapse
 
protium profile image
protium • Edited

Now it supports glob patterns (without flags) 💪🏼
github.com/protiumx/protiumx.githu...

Collapse
 
milanobrtlik profile image
Milan Obrtlik

It's super cool!
For us, developers.

What about people not familiar with CLI? E.g. project managers, HR... You bring hard times to them really quickly :D

Collapse
 
protium profile image
protium

Actually this was also intended for them. I truly think recruiters should have a good grasp on the tech side