Skip to content

Commit bac3cb7

Browse files
authored
Merge pull request adamj9431#3 from adamj9431/python2-support
added support for Python 2
2 parents f72d5df + 1d4fd7b commit bac3cb7

File tree

8 files changed

+319
-7
lines changed

8 files changed

+319
-7
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# notebook_xterm
22
[![PyPI version](https://badge.fury.io/py/notebook-xterm.svg)](https://badge.fury.io/py/notebook-xterm)
33

4-
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.
4+
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`).
55

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

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

1313
----
1414

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

3030
## Tested Environments
31-
+ Python 3
3231
+ [IBM Data Science Experience](https://datascience.ibm.com/)
3332
+ Jupyter 4.3.0
33+
+ Python 2 and 3
3434

3535
## License
3636
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details

notebook_xterm/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""A fully-functional terminal emulator in a Jupyter notebook."""
2-
__version__ = '0.1.2'
2+
__version__ = '0.2.0'
33
from .xterm import Xterm
44

55
def load_ipython_extension(ipython):

notebook_xterm/terminalclient.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ TerminalClient.prototype.receive_data_callback = function(data) {
128128
if (data.content.ename && data.content.evalue) {
129129
message += data.content.ename + ": " + data.content.evalue + "\r\n";
130130
data.content.traceback.map(function(row){
131+
row = row.replace('\n', '\r\n')
131132
message += row + '\r\n';
132133
});
133134
} else {

notebook_xterm/terminalserver.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from __future__ import (absolute_import, division,
2+
print_function, unicode_literals)
3+
14
import pty, os, tty, termios, time, sys, base64, struct, signal
25
from fcntl import fcntl, F_GETFL, F_SETFL, ioctl
36

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

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

1825
#set the file reads to be nonblocking
1926
flags = fcntl(self.file, F_GETFL)

notebook_xterm/xterm.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from __future__ import (absolute_import, division,
2+
print_function, unicode_literals)
3+
14
import os
25
from .terminalserver import TerminalServer
36
from IPython.core.display import display, HTML

setup.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
from setuptools import setup
22

33
setup(name='notebook_xterm',
4-
version='0.1.2',
4+
version='0.2.0',
55
description='A fully-functional terminal emulator in a Jupyter notebook.',
66
url='http://github.com/adamj9431/notebook_xterm',
77
author='Adam Johnson',
88
author_email='adam.johnson@us.ibm.com',
99
license='MIT',
1010
packages=['notebook_xterm'],
1111
keywords='Jupyter xterm notebook terminal bash shell cli',
12-
python_requires='~=3.3',
12+
install_requires=[
13+
'future',
14+
],
1315
include_package_data=True,
1416
zip_safe=False)

testing/test_python2.ipynb

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": 1,
6+
"metadata": {
7+
"scrolled": false
8+
},
9+
"outputs": [
10+
{
11+
"name": "stdout",
12+
"output_type": "stream",
13+
"text": [
14+
"Obtaining file:///Users/adam/Projects/notebook_xterm\n",
15+
"Requirement already satisfied: future in /Users/adam/anaconda3/lib/python3.6/site-packages (from notebook-xterm==0.2.0)\n",
16+
"Installing collected packages: notebook-xterm\n",
17+
" Found existing installation: notebook-xterm 0.1.2\n",
18+
" Uninstalling notebook-xterm-0.1.2:\n",
19+
" Successfully uninstalled notebook-xterm-0.1.2\n",
20+
" Running setup.py develop for notebook-xterm\n",
21+
"Successfully installed notebook-xterm\n"
22+
]
23+
}
24+
],
25+
"source": [
26+
"!pip install -e ../ "
27+
]
28+
},
29+
{
30+
"cell_type": "code",
31+
"execution_count": 2,
32+
"metadata": {},
33+
"outputs": [
34+
{
35+
"data": {
36+
"text/html": [
37+
"\n",
38+
" <div id=\"notebook_xterm\"></div>\n",
39+
" <script>var MAX_POLL_INTERVAL = 1500;\n",
40+
"var MIN_POLL_INTERVAL = 100;\n",
41+
"var BACKOFF_RATE = 1.8;\n",
42+
"var PY_XTERM_INSTANCE = 'get_ipython().find_magic(\"xterm\").__self__';\n",
43+
"var PY_TERMINAL_SERVER = PY_XTERM_INSTANCE + '.getTerminalServer()';\n",
44+
"function TerminalClient(elem) {\n",
45+
" this.closed = false;\n",
46+
" // require xterm.js\n",
47+
" require.config({\n",
48+
" paths: {\n",
49+
" xterm: '//cdnjs.cloudflare.com/ajax/libs/xterm/2.9.2/xterm.min'\n",
50+
" }\n",
51+
" });\n",
52+
"\n",
53+
" require(['xterm'], function(Terminal) {\n",
54+
" var termArea = this.create_ui(elem);\n",
55+
" this.term = new Terminal({\n",
56+
" rows: 25,\n",
57+
" cols: 100\n",
58+
" });\n",
59+
" this.term.open(termArea[0]);\n",
60+
"\n",
61+
" this.term.on('data', function(data) {\n",
62+
" this.handle_transmit(data);\n",
63+
" }.bind(this));\n",
64+
"\n",
65+
" this.term.on('resize', function() {\n",
66+
" this.handle_resize()\n",
67+
" }.bind(this));\n",
68+
"\n",
69+
" // set title\n",
70+
" this.term.on('title', function(title) {\n",
71+
" this.handle_title(title);\n",
72+
" }.bind(this));\n",
73+
"\n",
74+
" this.termArea.on('remove', function(ev) {\n",
75+
" this.close();\n",
76+
" }.bind(this))\n",
77+
"\n",
78+
" // set the initial size correctly\n",
79+
" this.handle_resize();\n",
80+
"\n",
81+
" // reset the terminal\n",
82+
" this.server_exec(PY_TERMINAL_SERVER + '.transmit(b\"' + btoa('\\r\\nreset\\r\\nclear\\r') + '\")');\n",
83+
"\n",
84+
" // start polling\n",
85+
" this.curPollInterval = MIN_POLL_INTERVAL;\n",
86+
" this.poll_server();\n",
87+
" console.log('Starting notebook_xterm.');\n",
88+
"\n",
89+
" }.bind(this));\n",
90+
"}\n",
91+
"\n",
92+
"TerminalClient.prototype.create_ui = function(elem) {\n",
93+
" var INITIAL_TITLE = 'notebook_xterm'\n",
94+
" // add xterm stylesheet for formatting\n",
95+
" var xtermCssUrl = 'https://cdnjs.cloudflare.com/ajax/libs/xterm/2.9.2/xterm.min.css'\n",
96+
" $('<link/>', {rel: 'stylesheet', href: xtermCssUrl}).appendTo('head');\n",
97+
"\n",
98+
" this.wrap = $('<div>').appendTo(elem);\n",
99+
" this.wrap.css({\n",
100+
" padding: 10,\n",
101+
" margin: 10,\n",
102+
" marginTop: 5,\n",
103+
" backgroundColor: 'black',\n",
104+
" borderRadius: 5\n",
105+
" });\n",
106+
" this.titleBar = $('<div>').appendTo(this.wrap);\n",
107+
" this.titleBar.css({\n",
108+
" color: '#AAA',\n",
109+
" margin: -10,\n",
110+
" marginBottom: 5,\n",
111+
" padding: 10,\n",
112+
" overflow: 'hidden',\n",
113+
" borderBottom: '1px solid #AAA'\n",
114+
" })\n",
115+
" this.titleText = $('<div>').html(INITIAL_TITLE).css({float: 'left'}).appendTo(this.titleBar);\n",
116+
" this.comIndicator = $('<div>').html('&middot;').css({float: 'left', marginLeft: 10}).hide().appendTo(this.titleBar);\n",
117+
" this.termArea = $('<div>').appendTo(this.wrap);\n",
118+
" return this.termArea;\n",
119+
"}\n",
120+
"\n",
121+
"TerminalClient.prototype.update_com_indicator = function() {\n",
122+
" this.comIndicator.show().fadeOut(400);\n",
123+
"}\n",
124+
"\n",
125+
"TerminalClient.prototype.server_exec = function(cmd) {\n",
126+
" if (this.closed) {\n",
127+
" return;\n",
128+
" }\n",
129+
"\n",
130+
" Jupyter.notebook.kernel.execute(cmd, {\n",
131+
" iopub: {\n",
132+
" output: function(data) {\n",
133+
" this.receive_data_callback(data)\n",
134+
" }.bind(this)\n",
135+
" }\n",
136+
" });\n",
137+
" // this.update_com_indicator();\n",
138+
"}\n",
139+
"\n",
140+
"TerminalClient.prototype.poll_server = function() {\n",
141+
" if (this.closed) {\n",
142+
" return;\n",
143+
" }\n",
144+
"\n",
145+
" this.server_exec(PY_TERMINAL_SERVER + '.receive()');\n",
146+
" clearTimeout(this.termPollTimer);\n",
147+
" this.termPollTimer = setTimeout(function() {\n",
148+
" this.poll_server();\n",
149+
" }.bind(this), this.curPollInterval);\n",
150+
" // gradually back off the polling interval\n",
151+
" this.curPollInterval = Math.min(this.curPollInterval*BACKOFF_RATE, MAX_POLL_INTERVAL);\n",
152+
"\n",
153+
" this.check_for_close();\n",
154+
"}\n",
155+
"TerminalClient.prototype.receive_data_callback = function(data) {\n",
156+
" if (this.closed) {\n",
157+
" return;\n",
158+
" }\n",
159+
"\n",
160+
" try {\n",
161+
" var decoded = atob(data.content.text);\n",
162+
" this.term.write(decoded);\n",
163+
" }\n",
164+
" catch(e) {\n",
165+
" var message = \"\\u001b[31;1m~~~ notebook_xterm error ~~~\\u001b[0m\\r\\n\"\n",
166+
" if (data.content.ename && data.content.evalue) {\n",
167+
" message += data.content.ename + \": \" + data.content.evalue + \"\\r\\n\";\n",
168+
" data.content.traceback.map(function(row){\n",
169+
" row = row.replace('\\n', '\\r\\n')\n",
170+
" message += row + '\\r\\n';\n",
171+
" });\n",
172+
" } else {\n",
173+
" message += \"See browser console for more details.\\r\\n\";\n",
174+
" }\n",
175+
" console.log(data.content);\n",
176+
" this.handle_title('error');\n",
177+
" this.term.write(message);\n",
178+
" this.close();\n",
179+
" }\n",
180+
"\n",
181+
"}\n",
182+
"TerminalClient.prototype.handle_transmit = function(data) {\n",
183+
" // we've had interaction, so reset the timer for the next poll\n",
184+
" // to minPollInterval\n",
185+
" this.curPollInterval = MIN_POLL_INTERVAL;\n",
186+
"\n",
187+
" // transmit data to the server, but b64 encode it\n",
188+
" this.server_exec(PY_TERMINAL_SERVER + '.transmit(b\"' + btoa(data) + '\")');\n",
189+
"}\n",
190+
"\n",
191+
"TerminalClient.prototype.handle_resize = function() {\n",
192+
" this.server_exec(PY_TERMINAL_SERVER + '.update_window_size('+ this.term.rows + ', '+ this.term.cols + ')');\n",
193+
"}\n",
194+
"\n",
195+
"TerminalClient.prototype.handle_title = function(title) {\n",
196+
" this.titleText.html(title);\n",
197+
"}\n",
198+
"TerminalClient.prototype.check_for_close = function() {\n",
199+
" if (!this.termArea.length) {\n",
200+
" this.close();\n",
201+
" }\n",
202+
"}\n",
203+
"TerminalClient.prototype.close = function() {\n",
204+
" if (this.closed) {\n",
205+
" return;\n",
206+
" }\n",
207+
" console.log('Closing notebook_xterm.');\n",
208+
" clearTimeout(this.termPollTimer);\n",
209+
" this.server_exec(PY_XTERM_INSTANCE + '.deleteTerminalServer()');\n",
210+
" this.closed = true;\n",
211+
"}\n",
212+
"// create the TerminalClient instance (only once!)\n",
213+
"if (window.terminalClient) {\n",
214+
" delete window.terminalClient;\n",
215+
"}\n",
216+
"window.terminalClient = new TerminalClient($('#notebook_xterm'))\n",
217+
"</script>\n",
218+
" "
219+
],
220+
"text/plain": [
221+
"<IPython.core.display.HTML object>"
222+
]
223+
},
224+
"metadata": {},
225+
"output_type": "display_data"
226+
}
227+
],
228+
"source": [
229+
"%load_ext notebook_xterm\n",
230+
"%xterm"
231+
]
232+
}
233+
],
234+
"metadata": {
235+
"kernelspec": {
236+
"display_name": "Python 2",
237+
"language": "python",
238+
"name": "python2"
239+
},
240+
"language_info": {
241+
"codemirror_mode": {
242+
"name": "ipython",
243+
"version": 2
244+
},
245+
"file_extension": ".py",
246+
"mimetype": "text/x-python",
247+
"name": "python",
248+
"nbconvert_exporter": "python",
249+
"pygments_lexer": "ipython2",
250+
"version": "2.7.14"
251+
}
252+
},
253+
"nbformat": 4,
254+
"nbformat_minor": 2
255+
}

testing/test_python3.ipynb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"metadata": {},
7+
"outputs": [],
8+
"source": [
9+
"!pip install -e ../ "
10+
]
11+
},
12+
{
13+
"cell_type": "code",
14+
"execution_count": null,
15+
"metadata": {},
16+
"outputs": [],
17+
"source": [
18+
"%load_ext notebook_xterm\n",
19+
"%xterm"
20+
]
21+
}
22+
],
23+
"metadata": {
24+
"kernelspec": {
25+
"display_name": "Python 3",
26+
"language": "python",
27+
"name": "python3"
28+
},
29+
"language_info": {
30+
"codemirror_mode": {
31+
"name": "ipython",
32+
"version": 3
33+
},
34+
"file_extension": ".py",
35+
"mimetype": "text/x-python",
36+
"name": "python",
37+
"nbconvert_exporter": "python",
38+
"pygments_lexer": "ipython3",
39+
"version": "3.6.3"
40+
}
41+
},
42+
"nbformat": 4,
43+
"nbformat_minor": 2
44+
}

0 commit comments

Comments
 (0)