Websockets are probably my favorite part of the new "HTML". Hopefully, the spec will be mature and widely-adopted soon, after passing over certain hurdles.
localStorage is another exciting new initiative that looks to be ready for primetime. localStorage doesn't require a server setup and is thus a bit easier to get started with than websockets.
We have a drum machine that is built on top of Twisted. Sometimes we use a twisted.conch.stdio.ConsoleManhole to send code to the reactor while it is running. In this way, there is the interesting possibility of coding improv. However, there are limitations to the usability of the ConsoleManhole for such ad hoc improvisation.
I decided to build a little webapp that will exchange messages with a ManholeInterpreter using websockets, based on the example server in txWebSocket. Basically, it sends the messages to the interpreter and gets back the output through a websocket connection. localStorage is used to save and restore different snippets.
The server automatically serves index.html for '/' and serves all static content from the directory it is run in.
The ManholeInterpreter takes a single argument that is a handler. The only requirement of this handler is that it implements an addOutput method, taking the data and, optionally, async. Here, we just make the websocket handler the interpreter handler and write to the transport the output of the interpreter. We run some startup code in our interpreter when we start it. The ManholeInterpreter is not a new interpreter, it is the same interpreter that the webserver is running in. So, unfortunately, you can't just restart everything by quitting it and attaching a new interpreter to the handler object. _ will probably fix this restart issue soon if he sees enough promise in the idea. For right now, if things get out of hand, you just have to restart the server and reload the page. Anyhoo ...
The server:
"""
#. Runs a websocket server that talks to the beatlounge console.
built on https://github.com/rlotun/txWebSocket
"""
import StringIO, sys, time
from datetime import datetime
from twisted.internet.protocol import Protocol, Factory
from twisted.python import log
from twisted.web import resource
from twisted.web.static import File
from twisted.internet import task
from twisted.conch.manhole import ManholeInterpreter
from websocket import WebSocketHandler, WebSocketSite
class CodeHandler(WebSocketHandler):
special_messages_to_method = {'restart': 'restart'}
def __init__(self, transport):
WebSocketHandler.__init__(self, transport)
self.startInterpreter()
def startInterpreter(self):
self.interpreter = ManholeInterpreter(self)
self.interpreter.runcode("""
from txbeatlounge.scheduler import clock as reactor
reactor.synthAudioDevice = "coreaudio"
reactor.run()
#from comps.core import *
import pprint
pp = pprint.PrettyPrinter(indent=2)
pp = pp.pprint
""")
def __del__(self):
print 'Deleting handler'
def frameReceived(self, frame):
""" """
#print 'Peer: ', self.transport.getPeer()
if not self.interpreter:
self.startInterpreter()
if frame in self.special_messages_to_method.keys():
getattr(self, self.special_messages_to_method[frame])()
else:
self.interpreter.runcode(frame)
def addOutput(self, data, async=False):
self.transport.write(data)
#log.msg(data)
def connectionMade(self):
print 'Connected to client.'
def connectionLost(self, reason):
print 'Lost connection.', reason
reason.printTraceback()
def restart(self):
"""Method that doesn't work @drew TODO :)
"""
self.interpreter.runcode("reactor.task.stop()")
self.interpreter.runcode("""
from txbeatlounge.instrument.fsynth import ChordPlayerMixin
l = locals()
for v in l.values():
if isinstance(v, ChordPlayerMixin):
v.stopall()
""")
if __name__ == "__main__":
from twisted.internet import reactor
log.startLogging(sys.stdout)
root = File('.')
site = WebSocketSite(root)
site.addHandler('/code', CodeHandler)
reactor.listenTCP(8080, site, interface='127.0.0.1')
reactor.run()
So, we have an index.html that is served at /:
<html>
<head>
<title>WebSocket Beatlounge</title>
<link rel="stylesheet" href="style.css" type="text/css">
</head>
<body>
<div id="left">
<h3>Send Text to the Beatlounge</h3>
<div id="conn_status">Not Connected</div>
<div id="textareaContainer">
<button id="runSaveButton">Run and update</button>
<button id="runOnly">Run without update</button>
<button id="inputReplace">Replace</button>
<br>
<textarea id="input" rows=20 cols=80></textarea>
</div>
<div id="restore"></div>
<div class="clear"></div>
<div id="inputRecordContainer">
<div id="save">
<input type="text" id="saveAs">
<button id="saveAsButton">Save</button>
</div>
<div id="inputRecordContainerButtons">
<button id="runInputDiv">Run</button>
<button id="editInputDiv">Edit</button>
<button id="clearCurrent">Clear the Current Code</button>
</div>
<div id="inputDiv"></div>
</div>
</div>
<div id="right">
<strong>Output/Preview</strong><button id="clearOutput">Clear</button>
<div id="output"></div>
</div>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" ></script>
<script type="text/javascript" src="./txbl.js"></script>
</body>
</html>
Now, recognize that this was all thrown together in a couple of days with no firm plan. A lot of the js that follows needs a refactor. However, I wanted to get this working version down before I completely tear it down for the next iteration. Future implementations will likely include some parts of Bespin, namely Skywriter. The save/restore is just a small clump of hacks, as well. However, the complete package works as intended and is an improvement for our purposes over the console manhole.
Here is the txbl.js:
var db = localStorage;
var ws = new WebSocket("ws://localhost:8080/code");
function nl2br_js(str) {
var regX = /\n/gi ;
s = new String(str);
return str;
}
function br2nl_js(str) {
var regX = /<br \/>/gi;
var regX2 = /<br>/gi;
var regX3 = /<br\/>/gi;
var trailingWhitespace = /\s*\n/g;
var trailingNew = /\n*$/g;
s = new String(str);
s = s.replace(regX, "");
s = s.replace(regX2, "");
s = s.replace(regX3, "");
s = s.replace(trailingWhitespace, "\n");
s = s.replace(trailingNew, "\n");
s = s.replace(/</g, "<")
s = s.replace(/>/g, ">");
return s;
}
function handle_storage(e) {
if (!e) { e = window.event; }
console.log('handle_storage');
console.log(e);
}
function show_saved() {
$('div#restore').html('');
if (db['___saved'] != undefined){
var arr = db['___saved'].split(':::');
for (var i=0; i < arr.length; i++) {
$('div#restore').append(
'<div class="restoreDelete"><a href="#" class="restoreA">'+arr[i]+'</a> <a href="#" class="deleteA">X</a></div>'
);
}
}
}
function record_container_buttons() {
if ($('#inputDiv').html() == '') {
$('#inputRecordContainer button').hide();
$('#inputRecordContainer #save').hide();
} else {
$('#inputRecordContainer button').show();
$('#inputRecordContainer #save').show();
}
}
function text_area_buttons() {
if ($('textarea#input').val() == '') {
$('#textareaContainer button').hide();
} else {
$('#textareaContainer button').show();
}
}
function show_current() {
$('#inputDiv').html(nl2br_js(db['___current']));
record_container_buttons();
}
function clear_and_focus() {
$('#output').html('');
$('textarea#input').val('');
$('textarea#input').focus();
}
$(function(){
db['___current'] = '';
show_current();
show_saved();
record_container_buttons();
if (window.addEventListener) {
window.addEventListener("storage", handle_storage, false);
} else {
window.attachEvent("onstorage", handle_storage);
};
ws.onmessage = function(evt) {
d = evt.data;
d = nl2br_js(d);
$('div#output').append('<p>'+d+'</p>');
}
ws.onopen = function(evt) {
$('#conn_status').html('<b>Connected</b>');
}
ws.onerror = function(evt) {
$('#conn_status').html('<b>Error</b>');
}
ws.onclose = function(evt) {
$('#conn_status').html('<b>Closed</b>');
}
// USER EVENTS
$('button#runSaveButton').click(function() {
var code = $('textarea#input').val();
ws.send(code);
db['___current'] = db['___current'] + code + '\n';
show_current();
//$('textarea#input').val('');
});
$('button#runOnly').click(function() {
var code = $('textarea#input').val();
ws.send(code);
//clear_and_focus();
});
$('button#inputReplace').click(function() {
db['___current'] = $('textarea#input').val();
show_current();
//clear_and_focus();
});
$('button#runInputDiv').click(function() {
var code = br2nl_js($('#inputDiv').html());
ws.send(code);
});
$('button#saveAsButton').click(function() {
var save_as = $('input#saveAs').val();
db[save_as] = db['___current'];
var saved_string = db['___saved'];
if (saved_string.match(':::'+save_as) == null && saved_string.match(save_as+':::') == null) {
db['___saved'] = db['___saved'] + ':::' + save_as;
}
show_saved();
});
$('a.restoreA').live("click", function() {
var name = $(this).html();
$('input#saveAs').val(name);
db['___current'] = db[name];
$('#output').html('');
//$('textarea#input').val(db['___current']);
show_current();
return false;
});
$('a.deleteA').live("click", function() {
var name = $(this).prev().html();
var saved = db['___saved'];
saved = saved.replace(name+':::', '');
saved = saved.replace(':::'+name, '');
db['___saved'] = saved;
db[name] = '';
show_saved();
return false;
});
$('button#clearCurrent').click(function() {
db['___current'] = '';
$('#inputDiv').html('');
$('input#saveAs').val('');
show_current();
});
$('button#editInputDiv').click(function() {
var input_html = $('div#inputDiv').html();
$('textarea#input').val(br2nl_js(input_html));
$('#output').html(input_html);
db['___current'] = '';
show_current();
});
$('button#clearOutput').click(function() {
$('div#output').html('');
});
});
Now for a touch of css:
body {min-width: 920px; }
#right {width: 50%; float: left;}
#output { white-space: pre; width: 100%; height: 90%; overflow-x: auto;}
#left {width: 49%; float: left; padding-right: 1%}
#inputRecordContainer {
width:100%; height: 38%; margin: 0.5em 0;
}
#inputRecordContainerButtons {
height: 25px;
}
#inputDiv {
white-space: pre;
margin: 0.5em 0; height:80%; overflow-x: auto; /*overflow-y:*/
}
.restoreDelete {
width: 150px;
float: left;
}
#restore { width: 100%; height: 12%; overflow-x: auto;}
.clear { clear: both; }
#textareaContainer { width: 100%;}
textarea#input { width: 100%;}
The result:
The real version of txbeatlounge with this server and more is private right now, unfortunately. We're probably going to push the new code to a publically available repo soon though. An older version can be acquired here.
you'll need https://github.com/rlotun/txWebSocket/blob/master/websocket.py on your path.