Building DAS-UIkeyboard-based visual programming language
screen

DAS-UI is another node-based experiment (after SDF-UI) that I've built during my one-project-a-month meta-project in 2017.

I've already built a few node-based UIs and I seem to quite like them (if you're curious about previous ones, you can read the intro to SDF-UI blogpost).

I spend most of my days in CLI, there's something hard to beat about text based interfaces, and I feel I'm much faster with keyboard shortcuts than with pointing and clicking.

Combining both of those worlds has been on my mind for quite some time, but as August began, and as I decided to work on this problem, I had no idea how to start.

sketch

I've spent a lot of time sketching and thinking, I didn't touch the code for a few days, as I was trying to get at least big-picture view on how this could work.

In hindsight, I was trying to solve to many things at once, I was thinking about UI, UX, and implementation (types? multiple inputs/outputs? streams? user-coded ui? blocks as simple functions? etc...) at the same time. I've finally decided that the biggest exploration here is going to be the UI/UX part, and focused on this for a while.

Since I didn't want to use mouse at all, it became obvious that there needs to be some kind of keyboard-controlled cursor. This cursor has to move by some predictable amounts, so I've added grid. Since there's a grid, I decided to make all the blocks always fit that grid. Blocks can still be dragged around and resized, but they always snap to predefined sizes.

The next important part was tackling the block creation.

I knew from the start that I would have little time to focus on actual blocks implementation, so I decided to allow users to create their own blocks.

So when creating a block, user can select either one from already existing block specs (more on that soon), or create a new spec, with custom functionality, that can later be re-used.

After this was working, the last thing missing was making connections between blocks. Here the biggest inspiration was how vimperator (and similar plugins) handle opening links: you first hit f which adds little tooltips around all the links with few letters, and type those letters to open the link. I used similar mechanism for connecting blocks: you first hover over the input or output, hit c, and then type matching letter to add that link.

DAS-UI is another project made with React, just because I know it well, and I know it's not going to block me when implementing new ideas. Whole state of application lives in Immutable.Map:

const initialState = fromJS({
  blockSpecs: {},
  graph: {
    blocks: {},
    connections: {}
  },
  ui: { ... }
});
  • blockSpecs is a map, pointing from name, to block spec info (inputs and outputs name), and stringified block implementation
  • graph handles actual blocks and connections on board, all of them accessible through uuid
  • ui containes all the other UI state, including cursor position, grid size, etc...

There are two intersting parts of the system that I'm going to describe in more details: block specs, and graph engine.

Block specs, meaning the block blueprint of sorts, are simple JavaScript object:

{
  name: 'some/name',
  inputs: [ 'first-input', 'another-input' ],
  outputs: [ 'some-output-values' ]
  code: ({ inputs, outputs, state }) => {},
  ui: ({ state, setState }) => {}
}
  • name is an identifier of the block, meaning that each some/name on board will have the same implementation
  • inputs and outputs are the arrays of names, they tell the UI to draw relevant items on the screen, and are also used to build the input and outputs objects
  • code is the implementation of the block (more below)
  • ui is an optional React component that will be rendered inside the block

code is the most interesting part here, this is where the block actually executes some logic. The argument to that functions is an object containing inputs, outputs and state — collections of RxJS streams. These colletions are constructed using inputs and outputs properties — each input and output gets its own stream. In the example above, it would mean that inputs is actually an object:

const inputs = {
  "first-input": new rx.Subject(),
  "another-input": new rx.Subject(),
};

Same thing happens with outputs. State is a stream that connects ui with code. ui can, through setState() call, update the state, which then code is notified about (through state.subscribe). If state was changed from code (with state.onNext), the ui gets re-rendered with updated state values.

This allowed me to keep the whole graph asynchronous, and have most of the piping work being taken care of by RxJS. Block's code can then easily .subscibe() to changes on one of the streams, and push new values out using .onNext(). This might be abuse of how rx.Subject()'s are supposed to work, I'm not an expert here, but it seems to work quite well.

Block specs themeselves are kept inside redux store. This wouldn't be needed if they were constnat, but I wanted users to be able to modify the specs on the go, and easily create new ones. To acheive that, the specs are kept in redux as strings. I've experimented with keeping functions inside the store, and stringifying/parsing them when storing to DB, but that proved to be not reliable and complex in implementation.

Block specs are executed using new Function constructor:

export const executeBlockSpec = (blockSpec) =>
  new Function(`return ${blockSpec.trim()}`)();

This simple function turns the block spec object into something resembling self-invoking anonymous function:

(function () {
  return {
    name: "some/name",
    //...
  };
})();

I can then capture the result, and use the block implementation.

Graph engine is what actualy runs the calculations. I keep graph as part of redux state: connections and blocks contain everything that exists on the board.

graph.blocks contain block's id, it's position, size, and name of the block spec that it uses. graph.connections contain only reference ids for connected blocks, and names of the input and output.

Graph engine is subscribed to store changes, and listens to changes in the graph subtree. Since the whole state is immutable, I can easily compare if anything has changed (if (!currentGraphState.equals(prevGraphState)) { ... }).

The next important part of the puzzle is immutable-diff package, which generates differences between two immutable structures. I first create the diff, and then walk through each of the changes deciding how to change running graph. The running graph also stores blocks and connections, the difference being that blocks point to actual running implementation (inlcluding reference for input and output streams for piping), and connections point to actual running conneciton, so it can be easily removed (this.connections[id].dispose()). Changes are not applied until the whole diff was walked through, this helped with a few bugs, where blocks would execute code with part-old, part-new state. I collect operations and arguments in futureOps array (for example futureOps.push([this.removeBlock, { id: blockId }])), and then, once I'm done with whole graph, I execute them one-by-one: futureOps.forEach(([op, arg]) => op(arg)).

One interesting thing to implement was a change in existing block spec — I wanted all other blocks using the same spec to be updated. To do that I look for all block ids and connection ids that have anything to do with that block spec, and then I remove them, and re-add them to the graph: services/graph.js:121-160

Block's state is also a part of the redux state, this allows me to know when to re-render the block's UI, and I can .subscribe() to changes in graph engine, and pipe them through state stream to block's code implementation.

This project, as all previous ones during my one-project-a-month thing, is far from complete.

I think of it as a proof-of-concept that keyboard-based node ui can be usable.

My next idea here would be to move it from web, to Electron, and use filesystem as a source of block's implementations (together with require support, etc..). This would allow me to write low-level block implementation in vim, and then do the high-level piping using something visual. I'd also love to tackle the problem of composing things into sub-patches, and reusing parts of graphs.

If you'd like to hack on DAS-UI it's open sourced and available on my Github: szymmonkaliski/DAS-UI.

You can read more about process behind making this project on the blog: building DAS-UI.