Nodejitsu

Save time managing and deploying your node.js app. Code faster with jitsu and npm

A blessed UI for Jitsu

About the author

Name
Location
Worldwide
nodejitsu nodejitsu

If you're reading this, it's likely you probably use a few terminal programs in your day-to-day work. Not just CLI programs, but full-fledged text UI programs (say vim or irssi, for example). Most everyone is familiar with writing a CLI program, but not many are familiar with the innerworkings of a TUI program. It requires a lot of esoteric knowledge, being that the machines originally used to render these programs were based off of earlier machines that resembled typewriters. Nowadays, we have programs which emulate these machines. We fittingly call them terminal emulators.

I'm a huge terminal nerd. I love tty-related things. I've written a terminal entirely in javascript. I've rewritten a terminal in C. I've rewritten parts of tmux to implement xterm behavior allowing the use of xterm terminfo, and I've even used some tmux code in my own projects.

The funny thing is, I haven't written that many programs that run inside terminals. In spite of my terminal obsession, I didn't know that much about curses or terminfo/termcap. Recently, I set to work on a terminfo and termcap parser. Why? I wasn't sure at first. It seemed very clear to me at the time that I was reinventing the wheel. Useless as it may seem, this ended up being the first step of writing what would become a high-level curses-like and widget library.

I tested my terminfo parser and compiler with every popular terminal's terminfo. Before I knew it, I had a fully-fledged tput clone:

$ tput.js setaf 2
$ tput.js sgr0
$ echo "$(tput.js setaf 2)hello world$(tput.js sgr0)"

As I progressed, I started to think reinventing the wheel wasn't entirely useless. It seems on most platforms, people would just use a curses binding for their terminal app needs. The problem with this in node.js is the overhead of switching between compiled JS and linked machine code. There are also benefits of easy extensibility and maintenance for a library in the javascript world. Not to mention, you don't have to compile anything. It was worth noting that ncurses isn't just a library that can output escape codes based on terminfo. It has many more features, but I decided I wanted to create something different. I wanted to create something that users of node could look at intuitively.

Blessed

blessed is a newschool curses-like library for node.js. It features a complete terminfo, extended terminfo, and termcap parser, so it will work with any terminal if need be. It also conforms to node's evented model. An example of this is the extension of node's readline module to listen not only for keys, but for all mouse event protocols (xterm, X10, SGR, rxvt, etc). In the higher-level API, it can listen for keypresses on focused elements, and mouse events on specific elements.

The lower level interface is very primitive.

var program = require('blessed')();

program.alternateBuffer();  
program.enableMouse();  
program.hideCursor();

program.bg('red');

program.on('mouse', function(data) {  
  if (data.action === 'mousedown') {
    program.move(data.x, data.y);
    program.write(' ');
  }
});

program.on('keypress', function(ch, key) {  
  if (key.name === 'q') {
    program.disableMouse();
    program.showCursor();
    program.normalBuffer();
    process.exit(0);
  }
});

A high-level widget API

So, the above seems nice, but it looks like it still requires low-level knowledge of how terminals work to easily create a UI. This is why I wanted to create not just a terminfo parser, but a high-level widget library as well. The goal here ended up becoming the creation of an API that can build fully functional TUI's without any knowledge of terminals. Nobody actually wants to think about where the terminal cursor is while attempting to design and render a UI.

Over the past several weeks, the API has, more or less, stabilized. The high-level API looks something like this:

var blessed = require('blessed');

// Create a screen object.
var screen = blessed.screen();

// Create a box perfectly centered horizontally and vertically.
var outer = blessed.box({  
  fg: 'blue',
  bg: 'default',
  border: {
    type: 'line',
    fg: '#ffffff'
  },
  tags: true,
  content: '{center}{red-fg}Hello{/red-fg}{/center}\n'
         + '{right}world!{/right}',
  width: '50%',
  height: '50%',
  top: 'center',
  left: 'center'
});

// Append our box to the screen.
screen.append(outer);

// Create a child box perfectly centered horizontally and vertically.
var inner = blessed.box({  
  parent: outer,
  top: 'center',
  left: 'center',
  width: '50%',
  height: '50%',
  border: {
    type: 'line',
    fg: '#ffffff'
  },
  fg: 'white',
  bg: 'magenta',
  content: 'Click {bold}me{/bold}!',
  tags: true,
  hoverEffects: {
    bg: 'green'
  }
});

// If our box is clicked, change the content.
inner.on('click', function(data) {  
  inner.setContent('{center}You clicked {red-fg}me{/red-fg}.{/center}');
  screen.render();
});

// If box is focused, handle `enter` and give us some more content.
inner.key('enter', function() {  
  inner.setContent('{right}You pressed {black-fg}enter{/black-fg}.{/right}\n');
  inner.setLine(1, 'bar');
  inner.insertLine(1, 'foo');
  screen.render();
});

// Quit on Escape, q, or Control-C.
screen.key(['escape', 'q', 'C-c'], function(ch, key) {  
  return process.exit(0);
});

// Focus our element.
inner.focus();

// Render the screen.
screen.render();  

The above code will create two boxes, one inside the other, perfectly centered vertically and horizontally, both of which will automatically resize when the terminal window resizes. The high level blessed API can also handle more interactive things like scrollable text boxes and lists. All of this is usable with barely any knowledge of the inner-workings of terminals.

blessed-nested-box

An example of a scrollable list:

var list = blessed.list({  
  parent: screen,
  width: '50%',
  height: '50%',
  top: 'center',
  left: 'center',
  align: 'center',
  fg: 'blue',
  border: {
    type: 'line'
  },
  selectedBg: 'green',

  // Allow mouse support
  mouse: true,

  // Allow key support (arrow keys + enter)
  keys: true,

  // Use vi built-in keys
  vi: true
});

list.setItems([  
  'one',
  'two',
  'three',
  'four',
  'five',
  'six',
  'seven',
  'eight',
  'nine',
  'ten'
]);

list.prepend(new blessed.Text({  
  left: 2,
  content: ' My list '
}));

// Allow scrolling with the mousewheel (manually).
// list.on('wheeldown', function() {
//   list.down();
// });
//
// list.on('wheelup', function() {
//   list.up();
// });

// Select the first item.
list.select(0);

screen.key('q', function(ch, key) {  
  return process.exit(0);
});

screen.render();  

blessed-list

Scrollable elements are dead simple. They even come bundled with built-in vi-style key binds if the element is focused.

var box = blessed.scrollabletext({  
  parent: screen,
  mouse: true,
  keys: true,
  vi: true,
  border: {
    type: 'line',
    fg: '#00ff00'
  },
  scrollbar: {
    fg: 'blue',
    ch: '|'
  },
  width: '50%',
  height: '50%',
  top: 'center',
  left: 'center',
  align: 'center',
  content: 'Loading...',
  tags: true
});

// Let's look at our own code.
request('https://raw.github.com/chjj/blessed/master/lib/widget.js', function(err, res, body) {  
  box.setContent(err ? '{red-fg}' + err.message + '{/}' : body);
  box.focus();
});

If you put it all together, you can have really complex widgets that only take seconds to whip up.

blessed-misc

API Design

Blessed is not the only high level terminal widget library. To ensure I created something unique and hopefully fine-tuned for node, I didn't pay much attention to other libraries when designing blessed.

So, the API here is somewhat reminiscent DOM/CSS(OM). The positioning behaves exactly the same as the CSS postion: absolute property. To go along with that, every element has left, right, up, and down properties. The only downside here is that elements are not aware of other elements at all. The position of one element cannot affect another's. This is something I may want to add in the future, but adds a lot of complexity. Every element can have a content propery, although, in many cases it may be beneficial to create a piece of text's own text element. Every element can also theoretically be scrollable via keys or mousewheel (if the right events are bound to).

One very important aspect that distinguishes this API, from something like the DOM for example, is the fact that it requires the user to refresh the screen by calling screen.render(). This allows for much more optimization and efficiency. If you're rendering 100 different elements one-by-one, there might be a lot of unnecessary bytes written to the terminal, but if you can buffer these actions, you can minimize the amount of work the terminal has to do.

The entire blessed API has been meticulously documented in the readme file. Although it is slightly unstable, following the documentation should serve you well.

A more approachable Jitsu

So why all this trouble to make a curses library and why is this on the nodejitsu blog? One of the things I wanted to do here at nodejitsu was to allow us to easily create TUI programs. Our main workhorse program and the heart of nodejitsu for all of our users is, of course, jitsu. jitsu-ui will be the terminal app for people who prefer a UI to a CLI.

jitsu-ui main

jitsu-ui deploy

jitsu-ui ask start

jitsu-ui starting

jitsu-ui logs

jitsu-ui snapshots

jitsu-ui env set

jitsu-ui env setting

Why not create a real GUI instead of a TUI? At the end of the day, terminals are more portable, and terminals aren't as primitive as people think. They have some sophisticated features people are unaware of. Mouse support is one example of an extremely high-level terminal feature people are unaware of it. Maybe you have seen programs with some mouse support, but I think people are unaware of how powerful the mouse protocols supported by terminals really are.

Jitsu-UI includes full mouse support. If you have a terminal properly replicating xterm or urxvt, everything is clickable and scrollable with the mousewheel. Buttons even have hover effects. I think a TUI crafted in this way can rival a GUI fairly well.

Jitsu-UI is fairly robust for a first version. It supports nearly everything Jitsu does, with planned support for some things jitsu doesn't do, such as built-in billing and support interfaces. I will be maintaining blessed and jitsu-ui for ages to come. If anyone wants to get involved with the development, please visit the jitsu github page or the blessed page.

If you want to check them out:

$ npm install -g jitsu-ui
$ npm install blessed