From d2e41bf57565d12f509e7f447ba7302c9ceb79c5 Mon Sep 17 00:00:00 2001 From: Steven Wittens Date: Mon, 20 Sep 2010 05:29:16 -0700 Subject: [PATCH] Spin off shell interaction to separate worker.js. Stream environment / commands between daemon and worker. --- HTML/client/client.js | 19 ++-- HTML/client/shell.js | 22 ++-- HTML/commandview/command.js | 9 +- HTML/commandview/commandcontext.js | 4 +- HTML/termkit.css | 28 ++++- HTML/termkit.js | 9 +- HTML/tokenfield/selection.js | 1 - Node/nodekit.js | 4 +- Node/processor.js | 123 ---------------------- Node/router.js | 101 ++++++++++++++++++ Node/shell/builtin.js | 48 +++++++++ Node/shell/shell.js | 157 +++++++++++----------------- Node/shell/worker.js | 161 +++++++++++++++++++++++++++++ Node/util.js | 17 +++ termkit.txt | 47 ++++++++- todo.txt | 22 ++-- 16 files changed, 505 insertions(+), 267 deletions(-) delete mode 100644 Node/processor.js create mode 100644 Node/router.js create mode 100644 Node/shell/builtin.js create mode 100644 Node/util.js diff --git a/HTML/client/client.js b/HTML/client/client.js index 38f0c05..585d91d 100644 --- a/HTML/client/client.js +++ b/HTML/client/client.js @@ -26,7 +26,7 @@ var tc = termkit.client = function () { // Message processing loop. s.on('message', function (data) { - self.message(data); + self.receive(data); }); // Open connection. @@ -35,31 +35,32 @@ var tc = termkit.client = function () { tc.prototype = { // Invoke a method on the server. - invoke: function (method, args, returnCallback, handlers) { + invoke: function (method, args, returnCallback, handlers, sessionId) { handlers = handlers || {}; handlers.Return = returnCallback; var sequence = this.nextId++; this.handlers[sequence] = handlers; - this.send(method, sequence, args); + this.send(sessionId, sequence, method, args); }, // Pass a message to the server. - send: function (method, sequence, args) { - var json = JSON.stringify([ method, sequence, args ]); + send: function (sessionId, sequence, method, args) { + var json = JSON.stringify([ sessionId, sequence, method, args ]); console.log('sending '+json); this.socket.send(json); }, // Receive a message from the server. - message: function (data) { + receive: function (data) { // Parse incoming message. var message = JSON.parse(data); if (message && message.length) { - var method = message[0], + var sessionId = message[0], sequence = message[1], - args = message[2]; + method = message[2], + args = message[3]; console.log('received '+data); @@ -70,7 +71,7 @@ tc.prototype = { if (handler) { var prefix = method.split('.')[0]; if (prefix == 'return') { - handler.Return && handler.Return(args.data, args.code, args.status); + handler.Return && handler.Return(args.data, args.code, args.status, sessionId); // Clean-up callbacks. delete this.handlers[sequence]; } diff --git a/HTML/client/shell.js b/HTML/client/shell.js index d973d5e..ed640cb 100644 --- a/HTML/client/shell.js +++ b/HTML/client/shell.js @@ -5,15 +5,27 @@ var tc = termkit.client; /** * NodeKit shell representation. */ -tc.shell = function (client, environment) { +tc.shell = function (client, environment, exit) { var self = this; this.client = client; this.environment = environment; - + + this.client.invoke('session.open.shell', { }, function (data, code, status, sessionId) { + self.sessionId = sessionId; + exit(); + }, this.hook()); }; tc.shell.prototype = { + + hook: function (handlers) { + var self = this; + handlers = handlers || []; + handlers['shell'] = function (m,a) { self.shellHandler(m, a); }; + return handlers; + }, + shellHandler: function (method, args) { switch (method) { case 'shell.environment': @@ -25,13 +37,9 @@ tc.shell.prototype = { }, run: function (tokens, exit, handlers) { - var self = this; - handlers['shell'] = function (m,a) { self.shellHandler(m, a); }; - this.client.invoke('shell.run', { tokens: tokens, - sessionId: this.environment.sessionId, - }, exit, handlers); + }, exit, this.hook(handlers), this.sessionId); }, }; diff --git a/HTML/commandview/command.js b/HTML/commandview/command.js index 32e944d..b263bcd 100644 --- a/HTML/commandview/command.js +++ b/HTML/commandview/command.js @@ -58,7 +58,12 @@ cv.command.prototype = { // Update the element. updateElement: function () { this.$element.data('controller', this); - this.$sigil.html(this.collapsed ? '▶' : '▼'); + var sigil = { + 'ok': '✔', + 'error': '✖', + 'warning': '⚠', + }[this.state]; + this.$sigil.attr('class', 'sigil sigil-'+this.state).html(this.collapsed ? '▶' : sigil); this.progressIndicator.$element[(this.state == 'running') ? 'show' : 'hide'](); }, @@ -168,7 +173,7 @@ cv.commandExecutable.triggerExecutable = function (offset, event, tokens) { }; cv.commandExecutable.autocompleteExecutable = function (offset, event, tokens, callback) { - var suggestions = []; + var suggestions = ['route', 'route6', 'view']; callback(suggestions); }; diff --git a/HTML/commandview/commandcontext.js b/HTML/commandview/commandcontext.js index e38bcd9..0c37d79 100644 --- a/HTML/commandview/commandcontext.js +++ b/HTML/commandview/commandcontext.js @@ -28,8 +28,8 @@ cv.commandContext.prototype = { // Update the element's markup in response to internal changes. updateElement: function () { this.$element.data('controller', this); - this.$path.html(escapeText(this.path)); - this.$user.html(escapeText(this.user)); + this.$path.html(escapeText(this.path || '')); + this.$user.html(escapeText(this.user || '')); }, set path(path) { diff --git a/HTML/termkit.css b/HTML/termkit.css index 720ea09..e77e1d8 100644 --- a/HTML/termkit.css +++ b/HTML/termkit.css @@ -69,6 +69,18 @@ body { color: #ccc; } +.termkitCommandView .command > span.sigil-ok { + color: #9f7; +} + +.termkitCommandView .command > span.sigil-warning { + color: #ff9; +} + +.termkitCommandView .command > span.sigil-error { + color: #f77; +} + .termkitCommandView .command .termkitProgressIndicator { position: absolute; left: -2px; @@ -82,7 +94,7 @@ body { border-width: 1px 0; line-height: 22px; padding: 10px 10px 10px 35px; - margin-bottom: -1px; + margin-top: -1px; cursor: text; } @@ -230,6 +242,11 @@ body { max-width: 100%; } +.termkitTokenField > span.token, +.termkitTokenField > span.token input { + text-shadow: 0 1px 2px rgba(0, 0, 0, .75); +} + .termkitTokenField > span.token:not(:first-child)::after { white-space: normal; content: ' '; @@ -252,7 +269,7 @@ body { margin: 0 2px 0 0; -webkit-box-shadow: 0px -1px 1px rgba(192,192,192,.6); background: #555 -webkit-gradient(linear, left top, left bottom, from(#666), to(#444)); - background: #222 -webkit-gradient(linear, left top, left bottom, from(#3a3a3a), to(#272727)); + background: #333 -webkit-gradient(linear, left top, left bottom, from(#4a4a4a), to(#333333)); color: #ddd; } @@ -261,9 +278,12 @@ body { -webkit-border-radius: .25em; margin: 0 3px 0 2px; -webkit-box-shadow: 0px -1px 1px rgba(160,180,192,1); - background: #578 -webkit-gradient(linear, left top, left bottom, from(#578), to(#356)); -/* background: #def -webkit-gradient(linear, left top, left bottom, from(#def), to(#bcd));*/ + background: #578 -webkit-gradient(linear, left top, left bottom, from(#579), to(#357)); color: #cef; + + -webkit-box-shadow: 0px -1px 1px rgba(192,192,192,1); + background: #666 -webkit-gradient(linear, left top, left bottom, from(#777), to(#444)); + color: #eee; } .termkitTokenField > span.token-empty span#caret input { diff --git a/HTML/termkit.js b/HTML/termkit.js index 431d0bd..6cee818 100644 --- a/HTML/termkit.js +++ b/HTML/termkit.js @@ -23,16 +23,11 @@ $(document).ready(function () { var client = new termkit.client(); client.onConnect = function () { - client.invoke('session.open.shell', {}, function (environment) { - var shell = new termkit.client.shell(client, environment); + var shell = new termkit.client.shell(client, {}, function () { var view = new termkit.commandView(shell); $('#terminal').append(view.$element); - view.newCommand(); - - console.log(environment); - }); - + }); }; diff --git a/HTML/tokenfield/selection.js b/HTML/tokenfield/selection.js index d11d672..068f059 100644 --- a/HTML/tokenfield/selection.js +++ b/HTML/tokenfield/selection.js @@ -114,7 +114,6 @@ tf.selection.fromEvent = function (event) { // Set position left or right. var center = (right + left) / 2; - $('body').append('
'+ center + ' ' + relativeOffset); return { token: token, offset: low + ((relativeOffset > center) ? 1 : 0) }; }; diff --git a/Node/nodekit.js b/Node/nodekit.js index f9cac76..0707d56 100644 --- a/Node/nodekit.js +++ b/Node/nodekit.js @@ -7,7 +7,7 @@ require.paths.unshift('.'); // Load requirements. var http = require('http'), io = require('socket.io') - processor = require("processor"); + router = require("router"); // Set up http server. var server = http.createServer(function (request, result) { @@ -21,7 +21,7 @@ server.listen(2222); // Set up WebSocket and handlers. var socket = io.listen(server); socket.on('connection', function (client) { - var p = new processor.router(server, client); + var p = new router.router(server, client); }); /* diff --git a/Node/processor.js b/Node/processor.js deleted file mode 100644 index af110ff..0000000 --- a/Node/processor.js +++ /dev/null @@ -1,123 +0,0 @@ -var shell = require("shell/shell"); - -/** - * Processes incoming messages on a connection and sends replies. - */ -exports.router = function (server, connection) { - this.connection = connection; - this.sessions = {}; - this.nextId = 1; - - this.handlers = { - 'session.open': function () { } - }; - - console.log('processor running'); - - var self = this; - connection.on('message', function (data) { - console.log('processor message '+ data); - self.message(data); - }); - connection.on('disconnect', function () { - console.log('processor disconnected'); - }); -}; - -exports.router.prototype = { - message: function (data) { - // Parse incoming message. - var message = JSON.parse(data); - if (message && message.length) { - var method = message[0], - sequence = message[1], - args = message[2]; - - // Verify arguments. - if (typeof message[1] == 'number') { - // Locate handler for method and execute. - if (exports.handlers[method]) { - // Create callback for sending reply messages. - var self = this, - invoke = function (method, args) { self.send(method, sequence, args); }, - // Look up session. - session = args.sessionId && this.getSession(args.sessionId), - - // Set up return handler. - exit = function (out) { - // Process shortcut return values - if (typeof out == 'object') { - out = { status: 'ok', code: 0, data: out }; - } - else if (out === false || out === null || out === undefined) { - out = { status: 'ok', code: 0 }; - } - else if (out === true) { - out = { status: 'error', code: 1 }; - } - else if (typeof out == 'number') { - out = { status: !out ? 'ok' : 'error', code: out }; - } - - // Return run status. - invoke('return', out); - }, - - // Execute method in context. - out = exports.handlers[method].call(this, args, invoke, exit, session); - - // If return value given, command has completed synchronously. - if (out !== undefined && out !== null) { - exit(out); - } - } - } - } - }, - - send: function (method, sequence, args) { - var json = JSON.stringify([ method, sequence, args ]); - console.log('sending '+json); - this.connection.send(json); - }, - - getSession: function (id) { - for (i in this.sessions) { - if (i == id) return this.sessions[id]; - } - }, - - addSession: function (session) { - var id = session.id = this.nextId++; - this.sessions[id] = session; - }, - - removeSession: function (session) { - delete this.sessions[session.id]; - }, -}; - -/** - * Method handlers. - */ -exports.handlers = { - 'session.open.shell': function (args, invoke, exit) { - var session = new shell.shell(this); - this.addSession(session); - - return session.environment; - }, - - 'session.close': function (args, invoke, exit, session) { - if (session) { - session.close(); - this.removeSession(); - return true; - } - return false; - }, - - 'shell.run': function (args, invoke, exit, session) { - return session.run(args, invoke, exit); - }, -}; diff --git a/Node/router.js b/Node/router.js new file mode 100644 index 0000000..6d56dec --- /dev/null +++ b/Node/router.js @@ -0,0 +1,101 @@ +var shell = require("shell/shell"); +var returnObject = require('util').returnObject; + +/** + * Processes incoming messages on a connection, routes them to active sessions. + */ +exports.router = function (server, connection) { + this.connection = connection; + this.sessions = {}; + this.nextId = 1; + + console.log('router running'); + + var self = this; + connection.on('message', function (data) { + console.log('router message '+ data); + self.receive(data); + }); + connection.on('disconnect', function () { + console.log('router disconnected'); + }); +}; + +exports.router.prototype = { + receive: function (data) { + // Parse incoming message. + var message = JSON.parse(data); + if (message && message.length) { + var sessionId = message[0], + sequence = message[1], + method = message[2], + args = message[3], + self = this; + + // Verify arguments. + if (typeof sequence == 'number') { + // Locate handler for method and execute. + if (exports.handlers[method]) { + // Look up session. + var session = sessionId && this.getSession(sessionId), + // Define convenient exit callback. + exit = function (value, object) { + if (object) { + value = [value, object]; + } + self.send('return', sequence, returnObject(value)); + }; + // Invoke method. + exports.handlers[method].call(this, session, sequence, args, exit); + } + } + } + }, + + send: function (sessionId, sequence, method, args) { + var json = JSON.stringify([ sessionId, sequence, method, args ]); + console.log('sending '+json); + this.connection.send(json); + }, + + getSession: function (id) { + for (i in this.sessions) { + if (i == id) return this.sessions[id]; + } + }, + + addSession: function (session) { + var id = session.id = this.nextId++; + this.sessions[id] = session; + }, + + removeSession: function (session) { + delete this.sessions[session.id]; + }, +}; + +/** + * Method handlers. + */ +exports.handlers = { + 'session.open.shell': function (session, sequence, args, exit) { + var session = new shell.shell(sequence, args, exit, this); + this.addSession(session); + }, + + 'session.close': function (session, sequence, args, exit) { + if (session) { + session.close(); + this.removeSession(session); + exit(false); + } + else { + exit(true); + } + }, + + 'shell.run': function (session, sequence, args, exit) { + session.run(sequence, args, exit); + // async exit + }, +}; diff --git a/Node/shell/builtin.js b/Node/shell/builtin.js new file mode 100644 index 0000000..0d4ccd0 --- /dev/null +++ b/Node/shell/builtin.js @@ -0,0 +1,48 @@ +exports.shellCommands = { + 'cd': function (tokens, invoke, exit) { + if (tokens.length != 2) { + return exit(true); + } + var path = tokens[1]; + + try { + process.chdir(path); + } + catch (error) { + return exit(true); + } + + this.sync(invoke); + exit(false); + }, +}; + + /* + // Resolve relative paths. + if (path[0] != '/') { + path = process.cwd() + '/' + path; + } + + // Fetch absolute path. + fs.realpath(path, function (err, path) { + console.log('realpath', path); + if (err) { + return exit(true); + } + + // See if path exists. + fs.stat(path, function (err, stats) { + console.log('stat', stats); + if (!err && stats.isDirectory()) { + self.cwd = path; + self.sync(invoke); + exit(); + } + else { + exit(true); + } + }); + }); + }, +}; +*/ \ No newline at end of file diff --git a/Node/shell/shell.js b/Node/shell/shell.js index 19d8194..1b7912e 100644 --- a/Node/shell/shell.js +++ b/Node/shell/shell.js @@ -1,117 +1,76 @@ -var fs = require('fs'); +var fs = require('fs'), net = require('net'); var spawn = require('child_process').spawn, exec = require('child_process').exec; -exports.shell = function (server, connection) { - //for (i in process.env) console.log(i + ' = '+process.env[i]); - this.environment = { - sessionId: 0, - cwd: process.env.HOME, - home: process.env.HOME, - user: process.env.USER, - uid: process.getuid(), - gid: process.getgid(), - path: process.env.PATH.split(':'), - manPath: process.env.MANPATH, - defaultShell: process.env.SHELL, - }; +exports.shell = function (sequence, args, exit, router) { - this.worker = new exports.shell.worker(this.environment); -}; + this.router = router; + this.buffer = ""; -exports.shell.prototype = { - - get id() { - return this.environment.sessionId; - }, - set id(id) { - this.environment.sessionId = id; - }, + var user = args.user || process.env.USER; + var self = this; - get cwd() { - return this.environment.cwd; - }, - set cwd(cwd) { - this.environment.cwd = cwd; - }, - - run: function (args, invoke, exit) { - var tokens = args.tokens; - console.log('shell.run: ' + tokens.join(' ')); - - var lead = tokens[0], handler; - if (handler = exports.shell.commands[lead]) { - handler.call(this, tokens, invoke, exit); - } - else { - // TODO: Replace with viewstream stderr equivalent - exit(true); - } - }, + // Extract location of source. + var p, path = process.argv[1].split('/'); + path[path.length - 1] = 'shell/worker.js'; + path = path.join('/'); - sync: function (invoke) { - invoke('shell.environment', this.environment); - }, -}; - -exports.shell.commands = { - 'cd': function (tokens, invoke, exit) { - if (tokens.length != 2) { - return exit(true); - } - var path = tokens[1], self = this; + // Determine user identity. + if (user == process.env.USER) { + console.log('spawning shell worker: ' + path); - // Resolve relative paths. - if (path[0] != '/') { - path = this.cwd + '/' + path; - } + // Spawn regular worker. + p = this.process = spawn('/usr/local/bin/node', [ path ], { + cwd: process.cwd(), + }); + } + else { + // Spawn sudo worker. + console.log('sudo not implemented'); + } - // Fetch absolute path. - fs.realpath(path, function (err, path) { - console.log('realpath', path); - if (err) { - return exit(true); - } - - // See if path exists. - fs.stat(path, function (err, stats) { - console.log('stat', stats); - if (!err && stats.isDirectory()) { - self.cwd = path; - self.sync(invoke); - exit(); - } - else { - exit(true); - } - }); + if (p) { + // Bind exit. + p.on('exit', function (code) { + console.log('shell worker exited with code ' + code); }); - }, -}; -exports.shell.worker = function (env) { - // Extract location of source. - var path = process.argv[1].split('/'); - path[path.length - 1 ] = 'shell/worker.js'; - path = path.join('/'); + // Bind receiver. + p && p.stdout.on('data', function (data) { self.receive(data); }); - if (env.user == process.env.USER) { - // Spawn regular worker. - spawn('node', [ path ], { cwd: process.cwd() }); + // Initalize worker. + this.send([sequence, 'init', args ]); } else { - // Spawn sudo worker. - + // Report error. + exit(true); + } +}; + +exports.shell.prototype = { + run: function (sequence, args) { + this.send([sequence, 'run', args ]); + }, + + close: function () { + this.process.stdin.close(); + }, + + send: function (data) { + var json = JSON.stringify(data); + this.process.stdin.write(json + "\u0000"); + }, + + receive: function (data) { + this.buffer += data; + while (this.buffer.indexOf("\u0000") >= 0) { + var chunk = this.buffer.split("\u0000").shift(); + console.log("parsing "+ data); + var data = JSON.parse(chunk); + this.router.send(this.id, data[0], data[1], data[2]); + this.buffer = this.buffer.substring(chunk.length + 1); + } } }; -/* -stats.isFile() -stats.isDirectory() -stats.isBlockDevice() -stats.isCharacterDevice() -stats.isSymbolicLink() (only valid with fs.lstat()) -stats.isFIFO() -stats.isSocket() -*/ \ No newline at end of file diff --git a/Node/shell/worker.js b/Node/shell/worker.js index e69de29..12a4bfe 100644 --- a/Node/shell/worker.js +++ b/Node/shell/worker.js @@ -0,0 +1,161 @@ +require.paths.unshift('.'); +require.paths.unshift('shell'); + +var builtin = require('builtin'); +var returnObject = require('util').returnObject; + +// Set up command processor. +var commandProcessor = function (commandStream, returnStream) { + // Set up stream callbacks. + var self = this; + commandStream.on('data', function (data) { self.data(data); }); + commandStream.on('end', function () { }); + this.returnStream = returnStream; + + this.buffer = ''; +}; + +commandProcessor.prototype = { + // Zero-delimited framing. + data: function (data) { + this.buffer += data; + while (this.buffer.indexOf("\u0000") >= 0) { + var chunk = this.buffer.split("\u0000").shift(); + this.receive(chunk); + this.buffer = this.buffer.substring(chunk.length + 1); + } + }, + + // Parse JSON command. + receive: function (data) { + try { + var message = JSON.parse(data); + if (message && message.length) { + var sequence = message[0], + method = message[1], + args = message[2], + self = this; + + if (typeof sequence == 'number') { + if (commandProcessor.handlers[method]) { + // Define convenient invocation callback. + var invoke = function (method, args) { + self.send(sequence, method, args); + }; + // Define convenient exit callback. + var exit = function (value, object) { + if (object) { + value = [value, object]; + } + invoke('return', returnObject(value)); + }; + // Invoke handler. + commandProcessor.handlers[method].call(this, args, invoke, exit); + } + } + } + } + catch (e) { + } + }, + + // Reply with method call. + send: function (sequence, method, args) { + var data = JSON.stringify([ sequence, method, args ]); + this.returnStream.write(data + "\u0000"); + }, + + // Sync up environment variables. + sync: function (invoke) { + var environment = { + cwd: process.cwd(), + home: process.env.HOME, + user: process.env.USER, + uid: process.getuid(), + gid: process.getgid(), + path: process.env.PATH.split(':'), + manPath: process.env.MANPATH, + defaultShell: process.env.SHELL, + }; + invoke('shell.environment', environment); + }, +}; + +commandProcessor.handlers = { + "init": function (args, invoke, exit) { + this.sync(invoke); + exit(false); + }, + "run": function (args, invoke, exit) { + var tokens = args.tokens; + + var lead = tokens[0], handler; + if (handler = builtin.shellCommands[lead]) { + handler.call(this, tokens, invoke, exit); + } + else { + // TODO: Replace with viewstream stderr equivalent + exit(true); + } + }, +}; + +// Set up streams. +var commandStream = process.openStdin(), + returnStream = process.stdout; + +var p = new commandProcessor(commandStream, returnStream); + +/* +SYNC +invoke('shell.environment', this.environment); +*/ + +/* +RUN + +*/ + +/* +var fs = require('fs'); + +if (process.argc < 3) { + return; +} + +// Set up additional in/out stream. +var self = this, + socketPath = '/tmp/termkit-worker-' + Math.floor(Math.random() * 1000000) +'.socket'; +var socketStream = net.createServer(function (stream) { + self.socketStream = stream; +}); +socketStream.listen(socketPath) + +// Set up worker configuration. +var config = { + socketPath: socketPath, + environment: env, +}; + +var config = JSON.parse(process.argv[2]); + +var commandStream = fs.createReadStream(config.socketPath, { + 'flags': 'r', + 'encoding': null, + 'mode': 0666, + 'bufferSize': 4 * 1024, +}); + +var viewStream = fs.createWriteStream(config.socketPath, { + 'flags': 'w', + 'encoding': null, + 'mode': 0666, + 'bufferSize': 4 * 1024, +}); + +console.log('commandStream ' + commandStream); +console.log('viewStream ' + viewStream); +// +// + +*/ \ No newline at end of file diff --git a/Node/util.js b/Node/util.js new file mode 100644 index 0000000..d92ec9b --- /dev/null +++ b/Node/util.js @@ -0,0 +1,17 @@ +// Process shortcut return values +exports.returnObject = function (out) { + if (out.constructor === [].constructor) { + out = { status: !out[0] ? 'ok' : 'error', code: out[0], data: out[1] }; + } + else if (out === false || out === null || out === undefined) { + out = { status: 'ok', code: 0 }; + } + else if (out === true) { + out = { status: 'error', code: 1 }; + } + else if (typeof out == 'number') { + out = { status: !out ? 'ok' : 'error', code: out }; + } + + return out; +}; diff --git a/termkit.txt b/termkit.txt index 6fa2ec5..333e069 100644 --- a/termkit.txt +++ b/termkit.txt @@ -126,13 +126,58 @@ problem: if front-end is agnostic, then how to make commands smarter? application/jsonstream ... ++++ Data vs UI glue + +e.g. + get http://.../rss.xml + content-type: application/xml + + -> process streams out xml data + -> must also generate/trigger viewing of xml tree + +[ ] A) Implied end-of-command pipe into default formatter for display. + Server-side handlers. Generic OS structures in shell/termkit namespace. App-specific, e.g. mysql handler turns query data into tables. + + Pro: complete separation of data and display + Pro: can pipe in any resource in the correct format. processing stages do not have to be termkit-capable. + Con: intermediate processes in pipeline cannot interact + +[X] B) Every program outputs viewstream parallel to datastream. invocation toggle to show only final output, or also intermediate? per-executable hints? + + Pro: view can be arbitrarily complex over simplistic data + Pro: two binaries which output the same datatypes can have radically different presentations + Con: need full wrapper around non-termkit processes + + chain of termkit / non-termkits: + + native bridge process autodetect + Rich Datastream -> Typed Binary Data -> Std Out -> Typed Data -> Rich Datastream -> OUT + Viewstream -> ---X -------> ViewStream -> OUT + + + get | grep + Data Stream: .txt|html|... > .txt|html|... + View Stream: text obj (size, lines) > preformattedtext view / html tree view + + get | ungzip | untar + Data Stream: .gz > .tar > nothing + View Stream: gzip obj (size) > tar archive (files) > list of file refs (extracted) + + Need generic default formatter for data-in. Takes mime blob and turns into static display. + - Binary stream (blob, image, zip, csv, ...) + - Url/path reference + - HTML/XML + - + + + serverside has hook system / listener system for manipulating references. this lets e.g. SVN output revision control flags on files transparently. this would apply across the entire shell view automatically. +++ UI stream structure > shell-specific interaction rules are client-side. > rich widget lib for display, extensible - > widgets are streamed to client like infograph-ML. objects are smartly typed and have callback commands defined for them. callbacks can be stateful or stateless. though stateful is only intended to be used for interactive commands. + > widgets are streamed to client like termkit-ML. objects are smartly typed and have callback commands defined for them. callbacks can be stateful or stateless. though stateful is only intended to be used for interactive commands. 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. diff --git a/todo.txt b/todo.txt index e801b49..f72cdcc 100644 --- a/todo.txt +++ b/todo.txt @@ -45,6 +45,7 @@ Prototype: [ ] host output [ ] auto-expand to fill view [ ] up/down to change focus + [ ] tabbing [ ] add forking the view with tabs and hosted/nested sessions 2) Command/token autocomplete @@ -68,16 +69,17 @@ Prototype: [X] keep state of sessions [X] export environment [X] allow path navigation - [ ] worker process - [ ] spawn process - [ ] stream in/out json packets - [ ] sudo support (askpass env?) -+ [.] make ls / cd commands - [ ] define datastream format - [ ] define viewstream format ++ [X] worker process + [X] refactor processor.js handler/return handling + [X] spawn process + [X] stream in/out json packets + [.] make ls / cd commands + [ ] define datastream format + [ ] define viewstream format + [ ] sudo support (askpass env?) [ ] Invoke a new process, capture output - - make a script for git (arguments, file/dir, ...) -bugs: -[ ] backspacing around token edges +5) Command Suite +[ ] make a script for git (arguments, file/dir, ...) +[ ] mysql console