DEV Community

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

Posted on • Edited 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:

👽

Latest comments (59)

Collapse
 
eekee profile image
Ethan Azariah

You could make randc display the images in-line as the w3m browser used to do with X11 xterm in the early 00s. :)

As someone who started using Linux in 1998 and has used a variety of command-line systems before and since, I find your command set interesting. :) If I hadn't known about the help command, it would have been the second thing I tried after repeated attempts at tab completion and maybe ls /bin too. (I'm sure even older Unix users would have been slightly disturbed by my use of tab completion. :p ) But help is a good command. I had to laugh when you declared your command list to be intuitive when the command to list file contents is called cat! ;) It certainly is intuitive for people familiar with the Unix command line and I can understand wanting to exclude others for certain purposes, but I lived through the era where everyone was talking about intuitive interfaces and so I had to laugh. :D

On a practical note, I see you mention glob patterns but cat doesn't support them. I used DOS heavily in the 90s and I use PowerShell a bit now, and I can tell you that inconsistencies in how different commands parse their command lines can be really uncomfortable. Unix systems have the shell handle pattern expansion, so it's always consistent. After years of DOS, I found that to be a relief. But why do I want cat to take patterns? I find typing unfamiliar words accurately to be hard, so whenever I'm stuck without tab completion, I use patterns. It's become a habit because sometimes, patterns are quicker.

This might be a bug:
In Firefox, open blog.md opens a complicated XML file with no style information. Browser info; (thanks for the handy command):
anonymous@protiumx.dev:~$ uname
Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0

Outside the terminal, I'm sorry to say I find the blog pages hard to read. My eyes don't adjust well to low contrast, and when I try to read it anyway, I get unpleasantly dazzled by brighter bits. I had the same problem with DOS in the 90s; it's a good thing I never had to use it for work. In Linux, when I had too little disk space for both Emacs and X Windows, I chose X Windows because it had a configurable xterm. In both DOS and Linux, there were a lot of programs I just didn't use because I couldn't make their displays comfortable. But on the web in recent years, I used to use a contrast enhancer but the only good one of those turned out to include some really nasty spyware. I should look again to see if others have got good.

Collapse
 
protium profile image
protium

Thank you for the insightful comment!
My bad, I forgot that the blog.md file is actually “virtual”, parsed from the RSS feed. I will add a fix later.

Tab autocomplete shouldn’t be difficult to implement, I might take a look as well.
Cheers!

Collapse
 
goodevilgenius profile image
Dan Jones

Cool. Didn't work in Firefox Mobile, though. After typing a command and hitting enter, the command disappeared and nothing happened.

Collapse
 
sria91 profile image
Srikanth Anantharam

The terminal doesn't work on my android phone.

Collapse
 
akoshodi profile image
Akinwale Oshodi

Tab completion

Collapse
 
pandademic profile image
Pandademic

Amazing job , and great article!

I'd just like to report a typo:

Also, cool web apps contain photos of cats, so I decided to write a randc command what will open a rando photo of a cat.

It say's 'rando' , I believe you meant to put random?

Anyway ,

Cheers and nice portfolio!

Collapse
 
msc profile image
Mario Sanchez Carrion

Awesome idea! My personal website is pretty minimal, but this is definitely taking things to the next level.

Intuitively, my first command was "ls", and it worked. Then I tried "nano" to open the files. When that didn't work I tried "cat" and it worked fine (also pretty intuitive).

Where I had more trouble is trying to open the resume.pdf file, since cat doesn't recognize the pdf format. I then tried "open" just for the sake of it and it gave me a list of arguments, including resume (without the extension), so "open resume" did work. To make it more intuitive perhaps one upgrade could be allowing the opening of files using the extension (.md or .pdf).

Really great project!

Collapse
 
protium profile image
protium

That's a good idea actually, I added it, it should be live now
Thank you for your comment!

Collapse
 
khuongduybui profile image
Duy K. Bui

hey I wanna report a serious bug! I ran the command "whoami" but got the output from "whoareyou" instead!!!

Nicely done my friend.

Collapse
 
protium profile image
protium

haha that was a dilemma to me. it makes sense to use whois but whoami is well known for printing information about current the user. in this case I changed the command description to current developer

Collapse
 
dinniej profile image
Nguyen Ngoc Dat

Pinned, will try to make one my own

Collapse
 
protium profile image
protium

Would love to see the results :D

Collapse
 
hq063 profile image
Gonzalo HQ063

Nice idea, however non tech people might never even realize they can write help to get a list of available commands and might quit without being able to do any interaction.
I would add some legend on terminal welcome message.

Also, open resume opens a 404 page on protiumx.dev/resume.pdf

Collapse
 
protium profile image
protium • Edited

I actually made it for tech people and recruiters only, since it has mostly data about my work experience or blog posts.
Thanks for the catch! I moved that file and forgot to update the url

Collapse
 
ashleyjsheridan profile image
Ashley Sheridan

While it is really cool, who is it for? Recruiters and hiring managers tend to be not very technical, and even of those who are, they would have to know how to use shell commands.