Harry Parton

The interesting bits of Terminal Reddit

Terminal Reddit was an idea that originally started life as a Hackernews reader... Yep. I wanted to try and use Vue to make something a bit different to the usual to-do/list example. Unfortunately early concepts with Hackernews lacked the interactivity that I wanted, so I switched to reddit to give me more possible commands.

The site uses Vue 2 for the interface, a basic version of the VueX pattern for state management and axios for some nice promise based API calling.

Terminal Interface

The terminal works by using a v-for loop on the data.responses, each entry has a type and a content. If the type is a command then we can output a prompt component and pass "false" to the active prop, rendering a copy of the prompt with no interactivity.

<div v-for="item in responses" class="terminal-line">
  <prompt v-if="item.type === 'command'" :active="false" :text="item.text" :directory="item.directory"></prompt>
  <response v-else :content="item.content" :type="item.type"></response>
</div>

Otherwise we make use of a factory function to create the appropriate response for the type. e.g. When showing an error message it's the type message so we just return the simple component which is basically just a line of text.

import table from './table'
import motd from './motd'
import help from './help'
import simple from './simple'

export default {
  functional: true,
  render (createElement, context) {
    function appropriateResponse () {
      var type = context.props.type

      if (type === 'list') return table
      if (type === 'motd') return motd
      if (type === 'help') return help

      // no component matches the type, default to simple
      return simple
    }

    return createElement(
      appropriateResponse(),
      {
        props: context.props
      },
      'response'
    )
  },
  props: ['content', 'type']
}

When we run a command, most will end with pushing a response object containing the type and the content to data.responses using a simple wrapper function, similar to doing a console.log()

this.createResponse(type, content)

Keeping documentation up to date.

The help command shows you a list of commands that you can use to get around, when I started with the project it was just left as a static component but after changing my mind on the syntax a few times I realised I needed to automate it instead.

When commands are registered you can register it with multiple aliases and register an example string.

/* 'message' is the type, 'howdy' is the content. */
this.command(['hi', 'hello', 'hey'], () => {
  this.createResponse('message', 'Howdy')
}, ['Say hello to the computer'])

I then pass a list of the registered commands from the terminal to the help output.

Making a custom prompt

I started off with just using a simple text input but didn't feel console-ey enough so ended up making a custom element that mirrors the value of an off-screen text input, this allows it to keep consistent styling across all browsers and lets us add some custom interactions.

Such as emulating the command history you normally get in bash, the prompt keeps track of every command you enter, then on arrow keyup cycles backwards through the history of commands.

The cons of creating a custom input is that we don't get anything for free, we have to have handlers for selecting and editing earlier text or the cursor for the mirror gets out of sync with the input.

Viewing posts

If the post is a reddit thread or the user wants to see comments then we can open it in a preview (shown above) that is very loosely based on the looks of the nano editor, otherwise we open it in a new window.

It turns out that browser vendors are smarter than me and realise letting you programmatically open windows with JavaScript is a terrible idea without permission. So I had to do a check and see if the window actually opens and if not, print a response telling the user to allow it rather than silently failing.

Browser security struck again with the second idea I had which was getting images, rendering them to canvas and turning that into ASCII art. Adding a cross-origin image to the canvas 'taints' it and you can't do getImageData() anymore. So that was that idea summarily shot in the head. Still trying to figure out a good workaround for this.

You can check out the live site here or take a look at the repo