Merge pull request #6614 from takluyver/nb-terminal-ui
Terminal emulator in the notebook
commit
5ff9ded54a
@ -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;
|
||||
}
|
||||
@ -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};
|
||||
});
|
||||
@ -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)
|
||||
Loading…
Reference in new issue