Browse files

viewstream emitters

  • Loading branch information...
1 parent b8e2230 commit af771596306a2efb39728a0a12dad166cccf9db8 @unconed unconed committed Apr 11, 2011
View
12 HTML/client/client.js
@@ -32,12 +32,12 @@ var tc = termkit.client = function () {
tc.prototype = {
- register: function (session, handler) {
- this.sessions[session.session] = handler;
+ add: function (session) {
+ this.sessions[session.id] = session;
},
deregister: function (session) {
- delete this.sessions[session.session];
+ delete this.sessions[session.id];
},
dispatch: function (message) {
@@ -49,9 +49,9 @@ tc.prototype = {
// must be regular viewstream message.
if (message.session) {
- var handler = this.sessions[message.session];
- if (handler) {
- handler(message);
+ var session = this.sessions[message.session];
+ if (session) {
+ session.dispatch(message.method, message.args);
}
}
},
View
71 HTML/client/shell.js
@@ -10,57 +10,100 @@ tc.shell = function (client, environment, success) {
this.client = client;
this.environment = environment;
- this.session = null;
+ this.id = null;
this.counter = 1;
+
+ this.frames = {};
+ this.views = {};
this.query('session.open.shell', { }, function (message) {
- that.session = message.args.session;
+ that.id = message.args.session;
+
+ that.client.add(that);
- console.log('session', that);
that.query('shell.environment', { }, function (message) {
that.environment = message.args;
success();
});
});
- this.client.register(that, function (message) { that.callback(message); });
-
};
tc.shell.prototype = {
-
+ close: function () {
+ this.process.stdin.end();
+ },
+
query: function (method, args, callback) {
- this.client.protocol.query(method, args, { session: this.session }, callback);
+ this.client.protocol.query(method, args, { session: this.id }, callback);
},
notify: function (method, args) {
- this.client.protocol.notify(method, args, { session: this.session });
+ this.client.protocol.notify(method, args, { session: this.id });
},
- // Handler for view.* invocations.
- callback: function (method, args) {
+ dispatch: function (method, args) {
+ var that = this;
+
console.log('viewstream', method, args);
switch (method) {
- case 'view.allocate':
+ case 'stream.open':
+ var frame = this.frames[args.rel];
+
+ // Allocate views.
+ if (frame) {
+ // Add views to viewstream list.
+ frame.allocate(args.streams.length);
+ for (i in args.streams) (function (id) {
+ view = frame.get(+i);
+ view.callback(function (method, args) {
+ // Lock callback to this view.
+ args.stream = id;
+
+ console.log('upstream', method, args);
+ that.notify(method, args);
+ });
+
+ that.views[id] = view;
+ })(args.streams[i]);
+ }
+ break;
+
+ case 'stream.close':
+ // Remove views from active viewstream list.
+ for (i in args.streams) {
+ delete this.views[args.streams[i]];
+ }
break;
+
+ default:
+ var view;
+ if (args.stream && (view = this.views[args.stream])) {
+ view.dispatch(method, args);
+ }
}
},
- run: function (tokens, exit) {
+ run: function (tokens, frame, exit) {
var that = this,
- ref = this.counter++,
+ rel = this.counter++,
callback = function (message) {
if (message.environment) {
that.environment = message.environment;
}
+
+ delete that.frames[rel];
+
exit(message.success, message.args, message);
};
+
+ this.frames[rel] = frame;
this.query('shell.run', {
tokens: tokens,
- ref: ref,
+ rel: rel,
}, callback);
},
};
View
29 HTML/commandview/command.js
@@ -92,22 +92,19 @@ cv.command.prototype = {
var command = tokens.map(function (t) { return t.toCommand(); });
// Execute in current context.
- this.context.shell.run(command, function (success, object, meta) {
- // Set appropriate return state.
- that.state = {
- '1': 'ok',
- '0': 'error',
- '-1': 'warning',
- }[+success] || 'ok';
-
- // Open new command.
- async(function () {
- that.commandView.newCommand();
- });
- },
- // Send all output to outputFrame.
- this.outputFrame.hook()
- );
+ this.context.shell.run(command, this.outputFrame, function (success, object, meta) {
+ // Set appropriate return state.
+ that.state = {
+ '1': 'ok',
+ '0': 'error',
+ '-1': 'warning',
+ }[+success] || 'ok';
+
+ // Open new command.
+ async(function () {
+ that.commandView.newCommand();
+ });
+ });
},
// Use triggers to respond to a creation or change event.
View
8 HTML/outputview/outputfactory.js
@@ -11,6 +11,12 @@ ov.outputFactory = function () {
ov.outputFactory.prototype = {
+ // Construct a tree of view objects.
+ tree: function (objects) {
+ var that = this;
+ return oneOrMany(objects).map(function (node) { return that.construct(node); });
+ },
+
construct: function (properties) {
var type = widgets[properties.type] || ov.outputNode,
node = new type(properties),
@@ -58,6 +64,8 @@ widgets.raw.prototype = $.extend(new ov.outputNode(), {
updateElement: function () {
this.$contents.text(this.properties.contents);
this.$element.data('controller', this);
+
+ this.notify('view.callback', { raw: 'foo' });
},
});
View
29 HTML/outputview/outputframe.js
@@ -20,27 +20,26 @@ of.prototype = {
return $outputFrame;
},
- // Hook into the given set of handlers.
- hook: function (handlers) {
- var that = this;
- handlers = handlers || {};
- handlers['view'] = function (m,a) { that.viewHandler(m, a); };
- return handlers;
+ // Get the n'th view in the frame.
+ get: function (i, callback) {
+ this.allocate(i + 1);
+ return this.views[i];
},
-
- viewHandler: function (method, args) {
- var subview = args.subview || 0;
- this.allocate(subview + 1);
- this.views[subview].viewHandler(method, args);
+
+ // Remove all views.
+ remove: function () {
+ this.$element.remove();
},
// Update the element's markup in response to internal changes.
- allocate: function (subviews) {
- if (this.views.length < subviews) {
- subviews -= this.views.length;
- while (subviews--) {
+ allocate: function (views) {
+ console.log('allocate from', this.views.length, ' to ', views);
+ if (this.views.length < views) {
+ views -= this.views.length;
+ while (views--) {
this.views.push(new termkit.outputView());
this.$element.append(this.views[this.views.length - 1].$element);
+ console.log('allocate -- ', this.views.length, views);
};
}
},
View
141 HTML/outputview/outputnode.js
@@ -5,7 +5,7 @@ var ov = termkit.outputView;
/**
* Represents a piece of output in an output view.
*/
-ov.outputNode = function (properties) {
+ov.outputNode = function (properties, root) {
this.properties = properties || {};
this.id = String(this.properties.id || '');
@@ -15,9 +15,9 @@ ov.outputNode = function (properties) {
this.children = [];
this.parent = null;
- this.root = this;
+ this.root = root || this;
- this.index = {};
+ this.map = {};
};
ov.outputNode.prototype = {
@@ -39,63 +39,37 @@ ov.outputNode.prototype = {
return this.children.length;
},
- // Adopt node index.
- mergeIndex: function (node) {
- for (id in node.index) {
- this.index[id] = node.index[id];
- }
- },
-
- // Adopt node index.
- unmergeIndex: function (node) {
- for (id in node.index) if (this.index[id]) {
- delete this.index[id];
- }
- },
-
- // Index node.
- indexNode: function (node) {
- if (node.id.length > 0) {
- this.index[node.id] = node;
- }
- },
-
- // Unindex node.
- unindexNode: function (node) {
- if (node.id.length > 0 && this.index[node.id]) {
- delete this.index[node.id];
- }
- },
-
// Link up node.
adopt: function (node) {
node.root = this.root;
node.parent = this;
- this.root.indexNode(node);
- this.root.mergeIndex(node);
-
+ if (node.id != '') {
+ this.map[node.id] = node;
+ }
+
node.updateElement();
},
// Detach node.
detach: function (node) {
- node.root = node;
+ node.root = null;
node.parent = null;
-
- this.root.unindexNode(node);
- this.root.unmergeIndex(node);
+
+ if (node.id != '') {
+ delete this.map[node.id];
+ }
},
// Insert node(s) inside this one.
- add: function (collection, index) {
+ add: function (collection, pointer) {
var that = this;
// Prepare splice call.
- if (typeof index != 'number') {
- index = this.children.length;
+ if (typeof pointer != 'number') {
+ pointer = this.children.length;
}
- var args = [ index, 0 ].concat(collection);
+ var args = [ pointer, 0 ].concat(collection);
// Allow both single object and array.
$.each(oneOrMany(collection), function () {
@@ -106,22 +80,27 @@ ov.outputNode.prototype = {
collection = collection.map(function (item) {
return item.$element[0];
});
- if (index >= this.children.length) {
+ if (pointer >= this.children.length) {
this.$children.append(collection);
}
else {
- this.$children.children()[index].before($(collection));
+ this.$children.children()[pointer].before($(collection));
}
// Add elements.
[].splice.apply(this.children, args);
- console.log(this.children);
},
- // Remove node at index.
- remove: function (index) {
+ // Remove node.
+ remove: function (pointer) {
+ // Self-remove?
+ if (typeof pointer == 'undefined') {
+ this.parent && this.parent.remove(this);
+ return;
+ }
+
// Locate node.
- var index = this.indexOf(index);
+ var index = this.indexOf(pointer);
var node = this.children[index];
if (node) {
@@ -134,15 +113,64 @@ ov.outputNode.prototype = {
}
},
- // Replace child node with this one.
- replace: function (node, index) {
+ // Replace self with another node(s).
+ replace: function (collection, pointer) {
+ // Self-replace
+ if (typeof pointer == 'undefined') {
+ var index = this.parent.indexOf(this);
+ this.parent && this.parent.remove(index);
+ this.parent.add(collection, index);
+ return;
+ }
+
+ // Locate node.
+ var index = this.indexOf(pointer);
+ var node = this.children[index];
+
this.remove(index);
- this.add(node, index)
+ this.add(collection, index);
},
- // Find index of given command in list.
- getNode: function (id) {
- return (typeof id == 'string' && id != '' && this.root.index[id]) || this;
+ // Update node's own properties.
+ update: function (properties) {
+ this.properties = $.extend({}, this.properties, properties || {});
+
+ this.root && this.updateElement();
+ },
+
+ /**
+ * Find target node.
+ *
+ * 'target' is an array of keys, matching one per level starting at this node.
+ * Keys can be integers (node index) or strings (node IDs).
+ */
+ getNode: function (target) {
+
+ console.log('getNode', target, typeof target);
+
+ if ((target == null) || (typeof target != 'object') || (target.constructor != [].constructor)) {
+ target = [target];
+ }
+ key = target.shift();
+
+ if (key == null) {
+ return this;
+ }
+
+ var types = {
+ string: 'map',
+ number: 'children',
+ };
+
+ var node, hash = types[typeof key];
+ if (hash) {
+ node = this[hash][key];
+ }
+
+ if (node && target.length) {
+ return node.getNode(target);
+ }
+ return node;
},
// Find index of given object in list.
@@ -159,6 +187,11 @@ ov.outputNode.prototype = {
prev: function (object) {
return this.children[this.indexOf(object) - 1];
},
+
+ // Notify callback for events.
+ notify: function (method, args) {
+ this.root && this.root.view && this.root.view.notify(method, args);
+ },
};
View
79 HTML/outputview/outputview.js
@@ -18,8 +18,8 @@ ov.prototype = {
var $outputView = $('<div class="termkitOutputView"><div class="isolate"></div></div>').data('controller', this);
var that = this;
- this.root = new ov.outputNode();
- $outputView.find('.isolate').append(this.root.$element);
+ this.tree = new ov.outputNode({}, this);
+ $outputView.find('.isolate').append(this.tree.$element);
return $outputView;
},
@@ -39,73 +39,46 @@ ov.prototype = {
},
- // Construct a tree of view objects.
- construct: function construct(objects) {
- var that = this;
- return oneOrMany(objects).map(function (node) { return that.factory.construct(node); });
- },
-
// Handler for view.* invocations.
- viewHandler: function (method, args) {
+ dispatch: function (method, args) {
+ var target = this.tree.getNode(args.target);
+ var nodes = args.objects && this.factory.tree(args.objects);
+
+ if (!target) return;
+
switch (method) {
case 'view.add':
- var target = this.root.getNode(args.target);
- var nodes = this.construct(args.objects);
- target.add(nodes);
- this.updateElement();
+ target.add(nodes, args.offset);
break;
case 'view.remove':
- var target = this.root.getNode(args.target);
- if (target.parent) {
- target.parent.remove(target);
- }
- this.updateElement();
+ target.remove();
break;
case 'view.replace':
+ target.replace(nodes);
+ break;
+
case 'view.update':
+ target.update(args.properties);
break;
}
+
+ this.updateElement();
+ },
+
+ // Notify back-end of callback event.
+ notify: function (method, args) {
+ this._callback && this._callback(method, args);
+ },
+
+ // Adopt new callback for sending back view events.
+ callback: function (callback) {
+ this._callback = callback;
},
};
///////////////////////////////////////////////////////////////////////////////
})(jQuery);
-
-
-
-/**
- * Add view object.
-add: function (target, offset) {
- var args = this.target(target, offset);
- args.contents = exports.prepareOutput(arguments[arguments.length - 1]);
- this.invoke('view.add', args);
-},
-
-/**
- * Remove view object.
-remove: function (target, offset) {
- var args = this.target(target, offset);
- this.invoke('view.remove', args);
-},
-
-/**
- * Replace view object.
-replace: function (target, offset) {
- var args = this.target(target, offset);
- args.contents = exports.prepareOutput(arguments[arguments.length - 1]);
- this.invoke('view.replace', args);
-},
-
-/**
- * Update view object.
-update: function (target, offset) {
- var args = this.target(target, offset);
- args.properties = arguments[arguments.length - 1];
- this.invoke('view.update', args);
-},
-
-*/
View
4 HTML/termkit.js
@@ -8,9 +8,9 @@ $(document).ready(function () {
var client = new termkit.client();
client.onConnect = function () {
- alert('client conn');
+ console.log('client conn');
var shell = new termkit.client.shell(client, {}, function () {
- alert('shell conn');
+ console.log('shell conn');
var view = new termkit.commandView(shell);
$('#terminal').append(view.$element);
view.newCommand();
View
12 HTML/tokenfield/token.js
@@ -334,30 +334,30 @@ tf.tokenRegex.prototype = $.extend(new tf.token(), {
tf.token.triggers = {
'*': [
{ changes: /./, callback: tf.tokenQuoted.triggerResetEscape },
- { changes: /./, callback: tf.tokenRegex.triggerResetEscape },
+// { changes: /./, callback: tf.tokenRegex.triggerResetEscape },
],
'empty': [
{ contents: /^["']/, callback: tf.tokenQuoted.triggerEscape },
{ contents: /["']/, callback: tf.tokenQuoted.triggerQuote },
- { contents: /^[\/]/, callback: tf.tokenRegex.triggerEscape },
- { contents: /[\/]/, callback: tf.tokenRegex.triggerRegex },
+// { contents: /^[\/]/, callback: tf.tokenRegex.triggerEscape },
+// { contents: /[\/]/, callback: tf.tokenRegex.triggerRegex },
{ contents: /./, callback: tf.tokenPlain.triggerCharacter },
{ contents: / /, callback: tf.tokenPlain.triggerEmpty },
],
'plain': [
{ contents: /^ ?$/, callback: tf.tokenEmpty.triggerEmpty },
{ changes: / /, callback: tf.tokenPlain.splitSpace },
{ changes: /["']/, callback: tf.tokenQuoted.triggerQuote },
- { changes: /[\/]/, callback: tf.tokenRegex.triggerRegex },
+// { changes: /[\/]/, callback: tf.tokenRegex.triggerRegex },
],
'quoted': [
{ changes: /["']/, callback: tf.tokenQuoted.triggerEscape },
{ changes: /["']/, callback: tf.tokenQuoted.triggerUnquote },
],
- 'regex': [
+/* 'regex': [
{ changes: /[\/]/, callback: tf.tokenRegex.triggerEscape },
{ changes: /[\/]/, callback: tf.tokenRegex.triggerUnregex },
- ],
+ ],*/
};
View
8 Node/shell/builtin.js
@@ -7,7 +7,7 @@ var fs = require('fs'),
exports.shellCommands = {
- 'cat': function (tokens, invoke, exit) {
+ 'cat': function (tokens, emitter, invoke, exit) {
// "cat <file> [file ...]" syntax.
if (true || tokens.length < 2) {
@@ -17,7 +17,7 @@ exports.shellCommands = {
},
- 'pwd': function (tokens, invoke, exit) {
+ 'pwd': function (tokens, emitter, invoke, exit) {
var out = new view.bridge(invoke);
var cwd = process.cwd();
@@ -26,7 +26,7 @@ exports.shellCommands = {
exit(true);
},
- 'cd': function (tokens, invoke, exit) {
+ 'cd': function (tokens, emitter, invoke, exit) {
var out = new view.bridge(invoke);
@@ -49,7 +49,7 @@ exports.shellCommands = {
exit(true);
},
- 'ls': function (tokens, invoke, exit) {
+ 'ls': function (tokens, emitter, invoke, exit) {
var out = new view.bridge(invoke);
View
49 Node/shell/command.js
@@ -8,43 +8,57 @@ var EventEmitter = require("events").EventEmitter,
whenDone = require('misc').whenDone,
returnObject = require('misc').returnObject,
- commandViewCounter = 1;
+ outputViewCounter = 1;
/**
* Represents a remote view for a command.
*/
-exports.commandView = function (processor) {
- this.id = commandViewCounter++;
+exports.outputView = function (processor) {
+ var id = this.id = outputViewCounter++;
- // Generate view invoke method.
+ // Generate 'view in' emitter for this view.
+ this.emitter = new EventEmitter();
+
+ // Generate 'view out' invoke method locked to one view.
this.invoke = function (method, args) {
- args.view = this.id;
+ args.stream = id;
processor.notify(method, args);
};
};
/**
* A pipeline of commands.
*/
-exports.commandList = function (processor, tokens, exit, ref) {
+exports.commandList = function (processor, tokens, exit, rel) {
if (tokens[0].constructor != [].constructor) {
tokens = [tokens];
}
- // Allocate views.
+ // Allocate n + 1 views.
var views = [], n = tokens.length;
for (var i = 0; i <= n; ++i) {
- view = new exports.commandView(processor);
+ view = new exports.outputView(processor);
views.push(view);
+
+ // Attach view's emitter to viewstream.
+ processor.attach(view.id, view.emitter);
}
- processor.notify('view.allocate', {
- ref: ref,
- views: views.map(function (v) { return v.id; }),
+
+ // Allocate view streams on client side.
+ processor.notify('stream.open', {
+ rel: rel,
+ streams: views.map(function (v) { return v.id; }),
});
- // Exit status tracker.
+ // Track exit of processes.
var returns = [],
track = whenDone(function () {
+ // Detach all views.
+ processor.notify('stream.close', {
+ streams: views.map(function (v) { return v.id; }),
+ });
+
+ // Return the last exit info to the shell.
exit.apply(null, returns);
});
@@ -58,7 +72,7 @@ exports.commandList = function (processor, tokens, exit, ref) {
returns = [ success, object ];
}
});
- return new exports.commandUnit.builtinCommand(command, views[i].invoke, exit);
+ return new exports.commandUnit.builtinCommand(command, views[i].emitter, views[i].invoke, exit);
});
// Spawn and link together.
@@ -87,8 +101,9 @@ exports.commandList.prototype = {
/**
* A single command in a pipeline.
*/
-exports.commandUnit = function (command, invoke, exit) {
+exports.commandUnit = function (command, emitter, invoke, exit) {
this.command = command;
+ this.emitter = emitter;
this.invoke = invoke;
this.exit = exit;
};
@@ -119,7 +134,7 @@ exports.commandUnit.prototype = {
/**
* Built-in command.
*/
-exports.commandUnit.builtinCommand = function (command, invoke, exit) {
+exports.commandUnit.builtinCommand = function (command, emitter, invoke, exit) {
exports.commandUnit.apply(this, arguments);
}
@@ -157,14 +172,14 @@ exports.commandUnit.builtinCommand.prototype.spawn = function () {
exports.commandUnit.builtinCommand.prototype.go = function () {
var that = this;
async(function () {
- that.handler.call(that, that.command, that.invoke, that.exit);
+ that.handler.call(that, that.command, that.emitter, that.invoke, that.exit);
});
};
/**
* UNIX command.
*/
-exports.commandUnit.unixCommand = function (command, invoke, exit) {
+exports.commandUnit.unixCommand = function (emitter, command, invoke, exit) {
exports.commandUnit.apply(this, arguments);
}
View
22 Node/shell/processor.js
@@ -10,9 +10,7 @@ var workerProcessor = exports.processor = function (inStream, outStream) {
this.outStream = outStream;
this.buffer = '';
-
- // Event emitters for callbacks.
- this.emitters = {};
+ this.streams = {};
};
exports.processor.prototype = {
@@ -74,6 +72,16 @@ exports.processor.prototype = {
}
},
+ // Establish a view stream.
+ attach: function (stream, emitter) {
+ this.streams[stream] = emitter;
+ },
+
+ // Drop a view stream.
+ detach: function (stream) {
+ delete this.streams[stream];
+ },
+
// Invoke an asynchronous method.
notify: function (method, args) {
var message = {
@@ -107,16 +115,20 @@ workerProcessor.handlers = {
"shell.run": function (args, exit) {
var that = this,
tokens = args.tokens,
- ref = args.ref;
+ rel = args.rel;
var shellExit = function (success, object, meta) {
meta = meta || {};
meta.environment = that.environment();
exit(success, object, meta);
};
- var list = new command.commandList(this, tokens, shellExit, ref);
+ var list = new command.commandList(this, tokens, shellExit, rel);
list.go();
},
+
+ "view.callback": function (args, exit) {
+
+ },
};
View
21 Node/view/view.js
@@ -21,18 +21,16 @@ exports.bridge.prototype = {
target: function (target, offset) {
var args = {
target: target,
+ offset: offset,
};
- if (typeof offset == 'numeric') {
- args.offset = offset;
- }
return args;
},
/**
* Add objects to a view object.
*/
- add: function (target, objects) {
- var args = this.target(target);
+ add: function (target, objects, offset) {
+ var args = this.target(target, offset);
args.objects = exports.prepareOutput(objects, true);
this.invoke('view.add', args);
},
@@ -46,10 +44,19 @@ exports.bridge.prototype = {
},
/**
+ * Replace an object with another view object.
+ */
+ replace: function (target, objects) {
+ var args = this.target(target);
+ args.objects = exports.prepareOutput(objects, true);
+ this.invoke('view.replace', args);
+ },
+
+ /**
* Update view object.
*/
- update: function (target, offset) {
- var args = this.target(target, offset);
+ update: function (target) {
+ var args = this.target(target);
args.properties = arguments[arguments.length - 1];
this.invoke('view.update', args);
},
View
50 termkit.txt
@@ -1,4 +1,4 @@
-TermKit
+ TermKit
+++ -
Goal: next gen terminal / command application
@@ -56,7 +56,7 @@ Good desktop citizen:
0.3: Command suite
[X] Redesign message protocol
- [ ] Viewstream integration
+ [X] Viewstream integration
[ ] 5-pipe command execution
[ ] unix command execution
[ ] filesystem autocomplete
@@ -132,9 +132,9 @@ For instance, programs don't communicate anything about the data they're sending
However, because most tools are launched by people for people, the default interchange format of choice is still "somewhat parseable text". As a result, we still have to be careful with things like spaces or Unicode in filenames, in case someone puts text where it doesn't belong. It still matters sometimes whether there is a newline at the end of a file. A misplaced keystroke, easily missed in the cryptic bash wash, can wreak havoc. Relying on text makes us prone to errors, and has lead to a culture where new recruits have to pass a constant trial by fire—to not destroy their system at one of the many opportunities it gives them.
-In fact, where it seems to matter most is the interaction. Text has literally locked down our abililty to evolve the UI, by forcing us to use the same monospace character terminals the previous generations used. Even worse, we are still limited to a headache inducing, fluorescent EGA color palette from 1984. A terminal can show you the load on your system, but it can't show you a historic graph. It can make little progress bars out of ASCII characters, but it can't show LaTeX math inline. Even the command-line itself with its clunky tab-complete, lack of text selection and standard copy/paste bears little resemblence to the standard text field widget we use every day in modern UIs.
+In fact, where it seems to matter most is the interaction. Text has literally locked down our abililty to evolve the UI, by forcing us to use the same monospace character terminals the previous generations used. Even worse, we are still often limited to a headache inducing, fluorescent EGA color palette from 1984. A terminal can show you the load on your system, but it can't show you a historic graph. It can make little progress bars out of ASCII characters, but it can't show LaTeX math inline. Even the command-line itself with its clunky tab-complete, lack of text selection and standard copy/paste bears little resemblence to the standard text field widget we use every day in modern UIs.
-In the past decade, user interaction has made astounding leaps. We consume and produce vast quantities of information daily. The web has changed from blobs of markup into full apps, with rich typography, complex infographics, inline interactivity, dynamic layout, etc. UIs like OS X have raised the bar on how we can present and interact with information naturally. Sure, fancy icons and smooth animations are great in iLife, but they can also be used in technical applications. You are almost certainly sitting in front of a display with more than a million pixels. Why are you telling it to draw a dinky character terminal from the 80s?
+In the past decade, user interaction has made astounding leaps. We consume and produce vast quantities of information daily. The web has changed from blobs of markup into full apps, with rich typography, complex infographics, inline interactivity, dynamic layout, etc. UIs like OS X have raised the bar on how we can present and interact with information naturally. Sure, fancy icons and smooth graphs are great in iLife, but they can also be used in technical applications. You are almost certainly sitting in front of a display with more than a million pixels. Why are you telling it to draw a dinky character terminal from the 80s?
<h2>A new model</h2>
@@ -157,7 +157,7 @@ After all this pontificating, it should be no surprise I'm building something to
<img>
-It consists of a desktop WebKit app acting as the front-end, with the entire UI built out of HTML/CSS and JavaScript. OS services like QuickLook are integrated through Cocoa.
+It consists of a desktop WebKit app acting as the front-end, with the UI built out of HTML/CSS and JavaScript. OS services like QuickLook are integrated through Cocoa.
On the flip side is a Node.js daemon, connected through Socket.IO, which maintains shells, runs programs and streams the visible output back to the front-end.
@@ -173,35 +173,36 @@ However, despite the clear power of HTML5, it would be a mistake to make HTML th
<h2>Smart views</h2>
-Instead, I wrote a View layer that acts like a simplified DOM. Using simple building blocks, like "item list", "table", "image" or "file with icon", the display is built. The idea is to have a wide range of UNIX concepts easily expressed, so it's easier to make nice UIs quickly. On the back end, a bridge makes it easy to interface by instantiating objects and streaming them to the front-end.
+Instead, I wrote a custom View layer. Using simple building blocks, like "item list", "sortable table", "image", "line graph" or "file with icon", the display is built. The idea is to have a wide range of UNIX concepts easily expressed, so it's easier to make nice UIs quickly. The View is interactive and can be updated asynchronously. Making a streaming dashboard is child's play for example.
+
+On the back end, a bridge makes it easy to interface by instantiating objects and streaming them to the front-end.
At the same time, you have access to HTML/CSS if you need it, or you can skip it altogether and just print plain text as well.
But what about the actual data? While it may sometimes be piped into a file, usually the data has to be displayed at the end. In the new model, data doesn't go to the terminal anymore, which complicates matters.
Imagine the case of "ls | grep": we're filtering items in a directory, which is displayed as a listing of files and icons. When the listing is output by "ls", sending it on the View out would send it directly to the terminal, preventing grep from filtering the items. If we instead pipe the items through grep, we lose the ability to view the data at all since it will remain in its raw form.
-To solve this, I annotate each data pipe with meta-data, using another unholy web ingredient: MIME. This allows me to keep the data pure, and identify it through its Content-Type and other values. In the case of "ls", we keep piping around plain-text filenames separated by newlines like before, but we annotate it so we can format it at the end of the command chain.
+To solve this, I use the meta-data added to each data pipe, with another unholy web ingredient: MIME. This allows me to keep the data pure, and identify it through its Content-Type and other values. In the case of "ls", we keep piping around plain-text filenames separated by newlines like before, but we annotate it so we can format it at the end of the command chain.
-The formatter reads the final data and turns it into a View graph, and can be extended to display files, images, JSON, source code, diffs, etc. This can be enabled for existing tools by deriving the Content-Type based on the command. It also makes TermKit web-transparent: you can hook up the data pipes to HTTP GET and POST, and the header information will be passed on.
+The formatter reads the final data and turns it into a View graph, and can be extended to display files, images, JSON, source code, diffs, etc. This can be enabled for existing tools by deriving the Content-Type based on the command. It also makes TermKit web-transparent: you can hook up the data pipes to HTTP GET and POST, and the header information will be correctly interpreted.
<h2>Possibilities</h2>
-In practice, the Data/View split has been there all along. It comes down to people vs programs, and the line is not hard to draw. "Wget" outputs a progressbar while streaming data into a file. "Top" displays an interactive dashboard that updates continuously. We often view changesets side by side, but still copy and pipe diffs around. In my model, each application can do both interactive tasks and data processing tasks at the same time, and do each in the most natural format for the job.
+In practice, the Data/View split has been there all along. It comes down to people vs programs, and the line is not hard to draw. "Wget" outputs a progressbar while streaming data into a file. "Top" displays an interactive dashboard that updates continuously. In this model, each application can do both interactive tasks and data processing tasks at the same time, and do each in the most natural format for the job.
-In addition, routing view commands this ways allows parallel or background processes to update the view independently in the scrollback, instead of colliding with each other and the prompt.
+Additionally, routing view commands this way allows parallel or background processes to update the view independently in the scrollback, instead of colliding with each other and the prompt.
The separation between front-end and back-end also brings other benefits: the back-end can be run remotely, tunneling the websocket over SSH to a local front-end. You get latency-free interaction for remote operations. Even more, we can replace the consoles-within-terminals of SQL and SFTP, and elevate them to first-class shells with all the benefits of the new interaction model.
-Doesn't this mean rewriting all our tools? No. Traditional Unix tools can be slotted in transparently, they just don't get the benefits of asynchronous output. Wrapper scripts can add additional functionality, e.g. to give GIT colored, side-by-side diffs or show version control status in an 'ls' listing.
+Doesn't this mean rewriting all our tools? Not necessarily. Traditional Unix tools can be slotted in transparently, they just don't get the benefits of asynchronous output. Wrapper scripts can add additional functionality, e.g. to give GIT colored, side-by-side diffs or show version control status in an 'ls' listing.
<h2>Status</h2>
I've been slowly working on TermKit for about a year now, but it's still mostly vaporware. When I started, I didn't know what the right solution would look like, I just knew I wanted something more modern and usable. There's been a lot of writing and rewriting of code just to get to this stage. It also doesn't help that I'm a code perfectionist in unfamiliar territory.
-The reason I wanted to blog about it is because all of the above is starting to sound like a really compelling argument to me. The idea is to create a flexible platform for making better tools, built out of cross-platform parts and with enough useful assumptions built-in to really make a difference.
-
-I'd love to hear feedback from Unix gurus to see if this is something you would use, or if there is something obvious I'm missing.
+The reason I wanted to blog about it is because all of the above is starting to sound like a really compelling argument to me. The idea is to create a flexible platform for making better tools, built out of cross-platform parts and with enough useful assumptions built-in to make a difference.
+Feedback is welcome, and I invite you to browse the GitHub repo which has mockups and some code diagrams.
+++ Protocol considerations
@@ -211,8 +212,9 @@ The output of a termkit command is split into data and view. The data is the raw
The view is a stream of dom-like objects and operators on them.
-There is an inherent assymmetry between view and
-
+View and data are fundamentally different:
+ * Data is a raw binary stream with meta-data annotation, from one process' stdout to another's stdin
+ * View is a packetized stream of UI updates and callback events, going directly to the terminal.
+++ Command architecture
@@ -248,13 +250,13 @@ problem: if front-end is agnostic, then how to make commands smarter?
stdin - data in. mime formatted
stdout - data out. mime formatted
- | events/sigs? ^ view stream: commandkit objects
- stdin v | "invoke" / "exit"
+ | events/sigs? ^ view stream: view updates
+ stdin v | "view.*" or return/exit
----> [ proc1: args = command? ] ---->
stdout
outgoing view stream is tagged with:
- * originating command = sequenceID (implied through invoke() / exit())
+ * originating command = query ID (wrapped through exit() helper)
* view ID per process
@@ -284,8 +286,8 @@ e.g.
native bridge process autodetect
Rich Datastream -> Typed Binary Data -> Std Out -> Typed Data -> Rich Datastream -> OUT
- Viewstream -> ---X -------> ViewStream -> OUT
-
+ Viewstream v v v ViewStream
+ terminal..........................................................
get | grep
Data Stream: .txt|html|... > .txt|html|...
@@ -398,12 +400,6 @@ method: view.*
tableview / listcontainer -> generic, scales form simple list to tabled headers w/ simple syntax
object references for files and other things. are multi-typed and annotated on server-side.
-
-+++ -- UI guidelines
-
-* Never ask Yes/No questions. Always use action verbs ('Delete', 'Don't Delete', 'Save and Quit', etc.)
-* Provide different levels of detail as different view models to toggle between.
-
+++ --
references:
View
14 todo.txt
@@ -1,5 +1,3 @@
-Redesigned message flow.
-
Tasks:
[ ] Viewstream integration
[X] outputview splittable into multiple isolated trees w/ subviews
@@ -11,9 +9,9 @@ Tasks:
[X] implement pipeline view alloc
[X] update client format
[X] update client callbacks
- [ ] update view/frame DOM handling
- [ ] do test run with CD
- [ ] do test run with LS
+ [X] update view/frame DOM handling
+ [X] do test run with CD
+ [X] do test run with LS
[ ] run pipeline test
[.] output formatter
[X] run built-in
@@ -111,7 +109,7 @@ Prototype:
[X] view parser/tree on client side
[X] list of files
[X] file icons
- [ ] implement view DOM v2 with path targeting
+ [X] implement view DOM v2 with path targeting
[ ] auto-layout mechanism w/ padded max-height on view
[ ] auto-layout column width (preferred size from widgets + widget variant/style)
[ ] make file icons cacheable long-term in webkit cache
@@ -132,8 +130,8 @@ Prototype:
[X] view proxy object on worker side
[X] make ls / cd commands
[X] simplify message format to raw json
- [ ] viewstream integration
-+ [ ] arrange pipes for command/view
+ [X] viewstream integration
+ [ ] arrange pipes for command/view
[ ] sudo support (askpass env?)

0 comments on commit af77159

Please sign in to comment.