Merge pull request #6614 from takluyver/nb-terminal-ui

Terminal emulator in the notebook
Min RK 11 years ago
commit 5ff9ded54a

@ -761,6 +761,15 @@ class NotebookApp(BaseIPythonApplication):
proto = 'https' if self.certfile else 'http'
return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
def init_terminals(self):
try:
from .terminal import initialize
initialize(self.web_app)
self.web_app.terminals_available = True
except ImportError as e:
self.log.info("Terminals not available (error was %s)", e)
self.web_app.terminals_available = False
def init_signal(self):
if not sys.platform.startswith('win'):
signal.signal(signal.SIGINT, self._handle_sigint)
@ -840,6 +849,7 @@ class NotebookApp(BaseIPythonApplication):
self.init_configurables()
self.init_components()
self.init_webapp()
self.init_terminals()
self.init_signal()
def cleanup_kernels(self):

@ -0,0 +1,17 @@
.terminal {
float: left;
border: black solid 5px;
font-family: "DejaVu Sans Mono", "Liberation Mono", monospace;
font-size: 11px;
color: white;
background: black;
}
.terminal-cursor {
color: black;
background: white;
}
#terminado-container {
margin: 8px;
}

@ -26,4 +26,4 @@
// notebook
@import "../notebook/less/style.less";
@import "../notebook/less/terminal.less";

@ -10483,4 +10483,19 @@ span#autosave_status {
-ms-transform: rotate(45deg);
-o-transform: rotate(45deg);
}
.terminal {
float: left;
border: black solid 5px;
font-family: "DejaVu Sans Mono", "Liberation Mono", monospace;
font-size: 11px;
color: white;
background: black;
}
.terminal-cursor {
color: black;
background: white;
}
#terminado-container {
margin: 8px;
}
/*# sourceMappingURL=../style/style.min.css.map */

@ -0,0 +1,53 @@
// Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
require([
'jquery',
'termjs',
'base/js/utils',
'base/js/page',
'terminal/js/terminado',
'custom/custom',
], function(
$,
termjs,
utils,
page,
terminado
){
page = new page.Page();
// Test size: 25x80
var termRowHeight = function(){ return 1.00 * $("#dummy-screen")[0].offsetHeight / 25;};
// 1.02 here arrived at by trial and error to make the spacing look right
var termColWidth = function() { return 1.02 * $("#dummy-screen-rows")[0].offsetWidth / 80;};
var base_url = utils.get_body_data('baseUrl');
var ws_path = utils.get_body_data('wsPath');
var ws_url = location.protocol.replace('http', 'ws') + "//" + location.host
+ base_url + ws_path;
var header = $("#header")[0]
function calculate_size() {
height = window.innerHeight - header.offsetHeight;
width = window.innerWidth;
var rows = Math.min(1000, Math.max(20, Math.floor(height/termRowHeight())-1));
var cols = Math.min(1000, Math.max(40, Math.floor(width/termColWidth())-1));
console.log("resize to :", rows , 'rows by ', cols, 'columns');
return {rows: rows, cols: cols};
}
page.show_header();
size = calculate_size();
var terminal = terminado.make_terminal($("#terminado-container")[0], size, ws_url);
page.show_site();
window.onresize = function() {
var geom = calculate_size();
terminal.term.resize(geom.cols, geom.rows);
terminal.socket.send(JSON.stringify(["set_size", geom.rows, geom.cols,
window.innerHeight, window.innerWidth]));
};
});

@ -0,0 +1,39 @@
define ([], function() {
function make_terminal(element, size, ws_url) {
var ws = new WebSocket(ws_url);
var term = new Terminal({
cols: size.cols,
rows: size.rows,
screenKeys: true,
useStyle: false
});
ws.onopen = function(event) {
ws.send(JSON.stringify(["set_size", size.rows, size.cols,
window.innerHeight, window.innerWidth]));
term.on('data', function(data) {
ws.send(JSON.stringify(['stdin', data]));
});
term.on('title', function(title) {
document.title = title;
});
term.open(element);
ws.onmessage = function(event) {
json_msg = JSON.parse(event.data);
switch(json_msg[0]) {
case "stdout":
term.write(json_msg[1]);
break;
case "disconnect":
term.write("\r\n\r\n[CLOSED]\r\n");
break;
}
};
};
return {socket: ws, term: term};
}
return {make_terminal: make_terminal};
});

@ -29,6 +29,7 @@
highlight: 'components/highlight.js/build/highlight.pack',
moment: "components/moment/moment",
codemirror: 'components/codemirror',
termjs: "components/term.js/src/term"
},
shim: {
underscore: {

@ -0,0 +1,57 @@
{% extends "page.html" %}
{% block title %}{{page_title}}{% endblock %}
{% block params %}
data-base-url="{{base_url}}"
data-ws-path="{{ws_path}}"
{% endblock %}
{% block site %}
<div id="terminado-container"></div>
{% endblock %}
{% block script %}
<!-- Hack: this needs to be outside the display:none block, so we can measure
its size in JS in setting up the page. It is still invisible. Putting in
the script block gets it outside the initially undisplayed region. -->
<!-- test size: 25x80 -->
<div style='position:absolute; left:-1000em'>
<pre id="dummy-screen" style="border: solid 5px white;" class="terminal">0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
<span id="dummy-screen-rows" style="">01234567890123456789012345678901234567890123456789012345678901234567890123456789</span>
</pre>
</div>
{{super()}}
<script src="{{ static_url("terminal/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
{% endblock %}

@ -0,0 +1,19 @@
import os
from terminado import NamedTermManager
from IPython.html.utils import url_path_join as ujoin
from .handlers import TerminalHandler, NewTerminalHandler, TermSocket
from . import api_handlers
def initialize(webapp):
shell = os.environ.get('SHELL', 'sh')
webapp.terminal_manager = NamedTermManager(shell_command=[shell])
base_url = webapp.settings['base_url']
handlers = [
(ujoin(base_url, "/terminals/new"), NewTerminalHandler),
(ujoin(base_url, r"/terminals/(\w+)"), TerminalHandler),
(ujoin(base_url, r"/terminals/websocket/(\w+)"), TermSocket,
{'term_manager': webapp.terminal_manager}),
(ujoin(base_url, r"/api/terminals"), api_handlers.TerminalRootHandler),
(ujoin(base_url, r"/api/terminals/(\w+)"), api_handlers.TerminalHandler),
]
webapp.add_handlers(".*$", handlers)

@ -0,0 +1,35 @@
import json
from tornado import web
from ..base.handlers import IPythonHandler, json_errors
class TerminalRootHandler(IPythonHandler):
@web.authenticated
@json_errors
def get(self):
tm = self.application.terminal_manager
terms = [{'name': name} for name in tm.terminals]
self.finish(json.dumps(terms))
class TerminalHandler(IPythonHandler):
SUPPORTED_METHODS = ('GET', 'DELETE')
@web.authenticated
@json_errors
def get(self, name):
tm = self.application.terminal_manager
if name in tm.terminals:
self.finish(json.dumps({'name': name}))
else:
raise web.HTTPError(404, "Terminal not found: %r" % name)
@web.authenticated
@json_errors
def delete(self, name):
tm = self.application.terminal_manager
if name in tm.terminals:
tm.kill(name)
# XXX: Should this wait for terminal to finish before returning?
self.set_status(204)
self.finish()
else:
raise web.HTTPError(404, "Terminal not found: %r" % name)

@ -0,0 +1,43 @@
"""Tornado handlers for the terminal emulator."""
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
import tornado
from tornado import web
import terminado
from ..base.handlers import IPythonHandler
class TerminalHandler(IPythonHandler):
"""Render the terminal interface."""
@web.authenticated
def get(self, term_name):
self.write(self.render_template('terminal.html',
ws_path="terminals/websocket/%s" % term_name))
class NewTerminalHandler(IPythonHandler):
"""Redirect to a new terminal."""
@web.authenticated
def get(self):
name, _ = self.application.terminal_manager.new_named_terminal()
self.redirect(name, permanent=False)
class TermSocket(terminado.TermSocket, IPythonHandler):
def get(self, *args, **kwargs):
if not self.get_current_user():
raise web.HTTPError(403)
# FIXME: only do super get on tornado ≥ 4
# tornado 3 has no get, will raise 405
if tornado.version_info >= (4,):
return super(TermSocket, self).get(*args, **kwargs)
def open(self, *args, **kwargs):
if tornado.version_info < (4,):
try:
self.get(*self.open_args, **self.open_kwargs)
except web.HTTPError:
self.close()
raise
super(TermSocket, self).open(*args, **kwargs)

@ -137,6 +137,7 @@ have['mistune'] = test_for('mistune')
have['requests'] = test_for('requests')
have['sphinx'] = test_for('sphinx')
have['jsonschema'] = test_for('jsonschema')
have['terminado'] = test_for('terminado')
have['casperjs'] = is_cmd_found('casperjs')
have['phantomjs'] = is_cmd_found('phantomjs')
have['slimerjs'] = is_cmd_found('slimerjs')
@ -264,6 +265,8 @@ if not have['jinja2']:
sec.exclude('notebookapp')
if not have['pygments'] or not have['jinja2']:
sec.exclude('nbconvert')
if not have['terminado']:
sec.exclude('terminal')
# config:
# Config files aren't really importable stand-alone

@ -162,6 +162,7 @@ def find_package_data():
pjoin(components, "underscore", "underscore-min.js"),
pjoin(components, "moment", "moment.js"),
pjoin(components, "moment", "min", "moment.min.js"),
pjoin(components, "term.js", "src", "term.js"),
pjoin(components, "text-encoding", "lib", "encoding.js"),
])

Loading…
Cancel
Save