Skip to content

Commit

Permalink
added support for Python 2
Browse files Browse the repository at this point in the history
  • Loading branch information
Adam Johnson committed Dec 5, 2017
1 parent f72d5df commit 1d4fd7b
Show file tree
Hide file tree
Showing 8 changed files with 319 additions and 7 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# notebook_xterm
[![PyPI version](https://badge.fury.io/py/notebook-xterm.svg)](https://badge.fury.io/py/notebook-xterm)

A fully-functional terminal emulator in an IPython/Jupyter notebook. This is useful for notebook environments that don't provide shell access. Uses [xterm.js](https://xtermjs.org) for a VT100-compliant Javascript terminal front-end component. Instead of an actual WebSocket, notebook_xterm uses the Javascript Jupyter cell execute function `Jupyter.notebook.kernel.execute()` as a channel to communicate between the Python runtime on the server (`TerminalServer`) and JavaScript runtime in the browser (`TerminalClient`). Note that currently this extension works only in Python 3 notebooks--Python 2 is not supported.
A fully-functional terminal emulator in an IPython/Jupyter notebook. This is useful for notebook environments that don't provide shell access. Uses [xterm.js](https://xtermjs.org) for a VT100-compliant Javascript terminal front-end component. Instead of an actual WebSocket, notebook_xterm uses the Javascript Jupyter cell execute function `Jupyter.notebook.kernel.execute()` as a channel to communicate between the Python runtime on the server (`TerminalServer`) and JavaScript runtime in the browser (`TerminalClient`).

![notebook_xterm_animation](https://user-images.githubusercontent.com/1238730/33512219-7d093170-d6f9-11e7-905f-480d62d17cd2.gif)

Expand All @@ -12,7 +12,7 @@ Check out [IBM Data Science Experience](https://datascience.ibm.com/) for a free

----

From within an IPython notebook (Python 3), install the package using pip:
From within an IPython notebook, install the package using pip:
```
!pip install notebook_xterm
```
Expand All @@ -28,9 +28,9 @@ To display a terminal, type the [magic function](http://ipython.readthedocs.io/e
```

## Tested Environments
+ Python 3
+ [IBM Data Science Experience](https://datascience.ibm.com/)
+ Jupyter 4.3.0
+ Python 2 and 3

## License
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
Expand Down
2 changes: 1 addition & 1 deletion notebook_xterm/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""A fully-functional terminal emulator in a Jupyter notebook."""
__version__ = '0.1.2'
__version__ = '0.2.0'
from .xterm import Xterm

def load_ipython_extension(ipython):
Expand Down
1 change: 1 addition & 0 deletions notebook_xterm/terminalclient.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ TerminalClient.prototype.receive_data_callback = function(data) {
if (data.content.ename && data.content.evalue) {
message += data.content.ename + ": " + data.content.evalue + "\r\n";
data.content.traceback.map(function(row){
row = row.replace('\n', '\r\n')
message += row + '\r\n';
});
} else {
Expand Down
9 changes: 8 additions & 1 deletion notebook_xterm/terminalserver.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from __future__ import (absolute_import, division,
print_function, unicode_literals)

import pty, os, tty, termios, time, sys, base64, struct, signal
from fcntl import fcntl, F_GETFL, F_SETFL, ioctl

Expand All @@ -13,7 +16,11 @@ def __init__(self):
tty.setraw(self.fd, termios.TCSANOW)

#open the shell process file descriptor as read-write
self.file = os.fdopen(self.fd,'wb+', buffering=0)
if sys.version_info >= (3, 0):
self.file = os.fdopen(self.fd,'wb+', buffering=0)
else:
#python 2 compatible code
self.file = os.fdopen(self.fd,'wb+', 0)

#set the file reads to be nonblocking
flags = fcntl(self.file, F_GETFL)
Expand Down
3 changes: 3 additions & 0 deletions notebook_xterm/xterm.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from __future__ import (absolute_import, division,
print_function, unicode_literals)

import os
from .terminalserver import TerminalServer
from IPython.core.display import display, HTML
Expand Down
6 changes: 4 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from setuptools import setup

setup(name='notebook_xterm',
version='0.1.2',
version='0.2.0',
description='A fully-functional terminal emulator in a Jupyter notebook.',
url='http://github.com/adamj9431/notebook_xterm',
author='Adam Johnson',
author_email='[email protected]',
license='MIT',
packages=['notebook_xterm'],
keywords='Jupyter xterm notebook terminal bash shell cli',
python_requires='~=3.3',
install_requires=[
'future',
],
include_package_data=True,
zip_safe=False)
255 changes: 255 additions & 0 deletions testing/test_python2.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"scrolled": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Obtaining file:///Users/adam/Projects/notebook_xterm\n",
"Requirement already satisfied: future in /Users/adam/anaconda3/lib/python3.6/site-packages (from notebook-xterm==0.2.0)\n",
"Installing collected packages: notebook-xterm\n",
" Found existing installation: notebook-xterm 0.1.2\n",
" Uninstalling notebook-xterm-0.1.2:\n",
" Successfully uninstalled notebook-xterm-0.1.2\n",
" Running setup.py develop for notebook-xterm\n",
"Successfully installed notebook-xterm\n"
]
}
],
"source": [
"!pip install -e ../ "
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
" <div id=\"notebook_xterm\"></div>\n",
" <script>var MAX_POLL_INTERVAL = 1500;\n",
"var MIN_POLL_INTERVAL = 100;\n",
"var BACKOFF_RATE = 1.8;\n",
"var PY_XTERM_INSTANCE = 'get_ipython().find_magic(\"xterm\").__self__';\n",
"var PY_TERMINAL_SERVER = PY_XTERM_INSTANCE + '.getTerminalServer()';\n",
"function TerminalClient(elem) {\n",
" this.closed = false;\n",
" // require xterm.js\n",
" require.config({\n",
" paths: {\n",
" xterm: '//cdnjs.cloudflare.com/ajax/libs/xterm/2.9.2/xterm.min'\n",
" }\n",
" });\n",
"\n",
" require(['xterm'], function(Terminal) {\n",
" var termArea = this.create_ui(elem);\n",
" this.term = new Terminal({\n",
" rows: 25,\n",
" cols: 100\n",
" });\n",
" this.term.open(termArea[0]);\n",
"\n",
" this.term.on('data', function(data) {\n",
" this.handle_transmit(data);\n",
" }.bind(this));\n",
"\n",
" this.term.on('resize', function() {\n",
" this.handle_resize()\n",
" }.bind(this));\n",
"\n",
" // set title\n",
" this.term.on('title', function(title) {\n",
" this.handle_title(title);\n",
" }.bind(this));\n",
"\n",
" this.termArea.on('remove', function(ev) {\n",
" this.close();\n",
" }.bind(this))\n",
"\n",
" // set the initial size correctly\n",
" this.handle_resize();\n",
"\n",
" // reset the terminal\n",
" this.server_exec(PY_TERMINAL_SERVER + '.transmit(b\"' + btoa('\\r\\nreset\\r\\nclear\\r') + '\")');\n",
"\n",
" // start polling\n",
" this.curPollInterval = MIN_POLL_INTERVAL;\n",
" this.poll_server();\n",
" console.log('Starting notebook_xterm.');\n",
"\n",
" }.bind(this));\n",
"}\n",
"\n",
"TerminalClient.prototype.create_ui = function(elem) {\n",
" var INITIAL_TITLE = 'notebook_xterm'\n",
" // add xterm stylesheet for formatting\n",
" var xtermCssUrl = 'https://cdnjs.cloudflare.com/ajax/libs/xterm/2.9.2/xterm.min.css'\n",
" $('<link/>', {rel: 'stylesheet', href: xtermCssUrl}).appendTo('head');\n",
"\n",
" this.wrap = $('<div>').appendTo(elem);\n",
" this.wrap.css({\n",
" padding: 10,\n",
" margin: 10,\n",
" marginTop: 5,\n",
" backgroundColor: 'black',\n",
" borderRadius: 5\n",
" });\n",
" this.titleBar = $('<div>').appendTo(this.wrap);\n",
" this.titleBar.css({\n",
" color: '#AAA',\n",
" margin: -10,\n",
" marginBottom: 5,\n",
" padding: 10,\n",
" overflow: 'hidden',\n",
" borderBottom: '1px solid #AAA'\n",
" })\n",
" this.titleText = $('<div>').html(INITIAL_TITLE).css({float: 'left'}).appendTo(this.titleBar);\n",
" this.comIndicator = $('<div>').html('&middot;').css({float: 'left', marginLeft: 10}).hide().appendTo(this.titleBar);\n",
" this.termArea = $('<div>').appendTo(this.wrap);\n",
" return this.termArea;\n",
"}\n",
"\n",
"TerminalClient.prototype.update_com_indicator = function() {\n",
" this.comIndicator.show().fadeOut(400);\n",
"}\n",
"\n",
"TerminalClient.prototype.server_exec = function(cmd) {\n",
" if (this.closed) {\n",
" return;\n",
" }\n",
"\n",
" Jupyter.notebook.kernel.execute(cmd, {\n",
" iopub: {\n",
" output: function(data) {\n",
" this.receive_data_callback(data)\n",
" }.bind(this)\n",
" }\n",
" });\n",
" // this.update_com_indicator();\n",
"}\n",
"\n",
"TerminalClient.prototype.poll_server = function() {\n",
" if (this.closed) {\n",
" return;\n",
" }\n",
"\n",
" this.server_exec(PY_TERMINAL_SERVER + '.receive()');\n",
" clearTimeout(this.termPollTimer);\n",
" this.termPollTimer = setTimeout(function() {\n",
" this.poll_server();\n",
" }.bind(this), this.curPollInterval);\n",
" // gradually back off the polling interval\n",
" this.curPollInterval = Math.min(this.curPollInterval*BACKOFF_RATE, MAX_POLL_INTERVAL);\n",
"\n",
" this.check_for_close();\n",
"}\n",
"TerminalClient.prototype.receive_data_callback = function(data) {\n",
" if (this.closed) {\n",
" return;\n",
" }\n",
"\n",
" try {\n",
" var decoded = atob(data.content.text);\n",
" this.term.write(decoded);\n",
" }\n",
" catch(e) {\n",
" var message = \"\\u001b[31;1m~~~ notebook_xterm error ~~~\\u001b[0m\\r\\n\"\n",
" if (data.content.ename && data.content.evalue) {\n",
" message += data.content.ename + \": \" + data.content.evalue + \"\\r\\n\";\n",
" data.content.traceback.map(function(row){\n",
" row = row.replace('\\n', '\\r\\n')\n",
" message += row + '\\r\\n';\n",
" });\n",
" } else {\n",
" message += \"See browser console for more details.\\r\\n\";\n",
" }\n",
" console.log(data.content);\n",
" this.handle_title('error');\n",
" this.term.write(message);\n",
" this.close();\n",
" }\n",
"\n",
"}\n",
"TerminalClient.prototype.handle_transmit = function(data) {\n",
" // we've had interaction, so reset the timer for the next poll\n",
" // to minPollInterval\n",
" this.curPollInterval = MIN_POLL_INTERVAL;\n",
"\n",
" // transmit data to the server, but b64 encode it\n",
" this.server_exec(PY_TERMINAL_SERVER + '.transmit(b\"' + btoa(data) + '\")');\n",
"}\n",
"\n",
"TerminalClient.prototype.handle_resize = function() {\n",
" this.server_exec(PY_TERMINAL_SERVER + '.update_window_size('+ this.term.rows + ', '+ this.term.cols + ')');\n",
"}\n",
"\n",
"TerminalClient.prototype.handle_title = function(title) {\n",
" this.titleText.html(title);\n",
"}\n",
"TerminalClient.prototype.check_for_close = function() {\n",
" if (!this.termArea.length) {\n",
" this.close();\n",
" }\n",
"}\n",
"TerminalClient.prototype.close = function() {\n",
" if (this.closed) {\n",
" return;\n",
" }\n",
" console.log('Closing notebook_xterm.');\n",
" clearTimeout(this.termPollTimer);\n",
" this.server_exec(PY_XTERM_INSTANCE + '.deleteTerminalServer()');\n",
" this.closed = true;\n",
"}\n",
"// create the TerminalClient instance (only once!)\n",
"if (window.terminalClient) {\n",
" delete window.terminalClient;\n",
"}\n",
"window.terminalClient = new TerminalClient($('#notebook_xterm'))\n",
"</script>\n",
" "
],
"text/plain": [
"<IPython.core.display.HTML object>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"%load_ext notebook_xterm\n",
"%xterm"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 2",
"language": "python",
"name": "python2"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.14"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
44 changes: 44 additions & 0 deletions testing/test_python3.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"!pip install -e ../ "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%load_ext notebook_xterm\n",
"%xterm"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.3"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

0 comments on commit 1d4fd7b

Please sign in to comment.