Skip to content

Commit 07e1abf

Browse files
committed
initial commit
0 parents  commit 07e1abf

File tree

9 files changed

+341
-0
lines changed

9 files changed

+341
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist
2+
.DS_Store

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2018 Chad Smith
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# pyxterm.js
2+
A fully functional terminal in your browser.
3+
4+
![screenshot](https://github.com/cs01/pyxterm.js/raw/master/pyxtermjs.gif)
5+
6+
This is a Flask/socket.io websocket backend combined with the Xterm.js Javascript terminal emulator frontend. It works out of the box.
7+
8+
## Installation
9+
10+
### Option 1
11+
This option installs system-wide or to your virtual environment. Should probably only be used if you're using a virtual environment.
12+
```
13+
pip install pyxtermjs
14+
pyxtermjs # run it from anywhere
15+
```
16+
17+
### Option 2
18+
This option installs system-wide and isolates all of pyxterm.js's dependencies, guaranteeing there are no dependency version conflicts. Requires [pipsi](https://github.com/mitsuhiko/pipsi) to be installed.
19+
```
20+
pipsi install pyxtermjs
21+
pyxtermjs # run it from anywhere
22+
```
23+
24+
### Option 3
25+
This option lets you play around with the source code. Requires [poetry](https://github.com/sdispater/poetry) to be installed.
26+
```
27+
git clone https://github.com/cs01/pyxterm.js.git
28+
cd pyxterm.js
29+
poetry install
30+
python pyxtermjs/app.py
31+
```

pyproject.lock

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
[[package]]
2+
category = "main"
3+
description = "A simple wrapper around optparse for powerful command line utilities."
4+
name = "click"
5+
optional = false
6+
platform = "*"
7+
python-versions = "*"
8+
version = "6.7"
9+
10+
[[package]]
11+
category = "main"
12+
description = "A simple framework for building complex web applications."
13+
name = "flask"
14+
optional = false
15+
platform = "any"
16+
python-versions = "*"
17+
version = "1.0.2"
18+
19+
[package.dependencies]
20+
Jinja2 = ">=2.10"
21+
Werkzeug = ">=0.14"
22+
click = ">=5.1"
23+
itsdangerous = ">=0.24"
24+
25+
[[package]]
26+
category = "main"
27+
description = "Socket.IO integration for Flask applications"
28+
name = "flask-socketio"
29+
optional = false
30+
platform = "any"
31+
python-versions = "*"
32+
version = "3.0.1"
33+
34+
[package.dependencies]
35+
Flask = ">=0.9"
36+
python-socketio = ">=1.6.1"
37+
38+
[[package]]
39+
category = "main"
40+
description = "Various helpers to pass trusted data to untrusted environments and back."
41+
name = "itsdangerous"
42+
optional = false
43+
platform = "UNKNOWN"
44+
python-versions = "*"
45+
version = "0.24"
46+
47+
[[package]]
48+
category = "main"
49+
description = "A small but fast and easy to use stand-alone template engine written in pure python."
50+
name = "jinja2"
51+
optional = false
52+
platform = "*"
53+
python-versions = "*"
54+
version = "2.10"
55+
56+
[package.dependencies]
57+
MarkupSafe = ">=0.23"
58+
59+
[[package]]
60+
category = "main"
61+
description = "Implements a XML/HTML/XHTML Markup safe string for Python"
62+
name = "markupsafe"
63+
optional = false
64+
platform = "UNKNOWN"
65+
python-versions = "*"
66+
version = "1.0"
67+
68+
[[package]]
69+
category = "main"
70+
description = "Engine.IO server"
71+
name = "python-engineio"
72+
optional = false
73+
platform = "any"
74+
python-versions = "*"
75+
version = "2.2.0"
76+
77+
[package.dependencies]
78+
six = ">=1.9.0"
79+
80+
[[package]]
81+
category = "main"
82+
description = "Socket.IO server"
83+
name = "python-socketio"
84+
optional = false
85+
platform = "any"
86+
python-versions = "*"
87+
version = "2.0.0"
88+
89+
[package.dependencies]
90+
python-engineio = ">=2.2.0"
91+
six = ">=1.9.0"
92+
93+
[[package]]
94+
category = "main"
95+
description = "Python 2 and 3 compatibility utilities"
96+
name = "six"
97+
optional = false
98+
platform = "*"
99+
python-versions = "*"
100+
version = "1.11.0"
101+
102+
[[package]]
103+
category = "main"
104+
description = "The comprehensive WSGI web application library."
105+
name = "werkzeug"
106+
optional = false
107+
platform = "any"
108+
python-versions = "*"
109+
version = "0.14.1"
110+
111+
[metadata]
112+
content-hash = "f853c6aa973084de10b3965150b69a2eeec3133eed17afa1036076841dafdb1d"
113+
platform = "*"
114+
python-versions = "^3.6"
115+
116+
[metadata.hashes]
117+
click = ["29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", "f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"]
118+
flask = ["2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", "a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"]
119+
flask-socketio = ["034c36bad808f93efd5b0aeafd740dc314b0a848f17e9ec6c387c74b3f22dfe7", "794426c85f5b79d66c3ca583b6185d334f1493e655a1a0f02eb893a56970afe4"]
120+
itsdangerous = ["cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"]
121+
jinja2 = ["74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", "f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"]
122+
markupsafe = ["a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"]
123+
python-engineio = ["abf0d11098643eb1278d2db4dfbd13c0fe3cbfa99315572fd1c223277df62f2e", "d35c50db73bf372f82975dc87f18f1cbfab7d5bed2dfc2553ae784579502a461"]
124+
python-socketio = ["3513110bc14db4c961c33af51169705bbaeead380ecafbe845275ac8c6f9395f", "bde24ef6132a3c2e1b24e77847b31d331abdf8da8c711f0ef9cee4785000e023"]
125+
six = ["70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", "832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"]
126+
werkzeug = ["c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", "d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"]

pyproject.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[tool.poetry]
2+
name = "pyxtermjs"
3+
4+
authors = ["Chad Smith <[email protected]>"]
5+
description = "interactive terminal in the browser"
6+
homepage = "https://github.com/cs01/pyxterm.js"
7+
keywords = ['xterm', 'javascript', 'terminal-emulators', 'browser', 'tty', 'pty', 'console', 'terminal']
8+
license = "MIT"
9+
readme = 'README.md'
10+
repository = "https://github.com/cs01/pyxterm.js"
11+
version = "0.1.0"
12+
13+
14+
[tool.poetry.dependencies]
15+
python = "^3.6"
16+
flask-socketio = "^3.0"
17+
18+
[tool.poetry.scripts]
19+
pyxtermjs = "pyxtermjs.app:main"

pyxtermjs.gif

150 KB
Loading

pyxtermjs/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = "0.1.0"

pyxtermjs/app.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#!/usr/bin/env python3
2+
from flask import Flask, render_template
3+
from flask_socketio import SocketIO
4+
import pty
5+
import os
6+
import subprocess
7+
import select
8+
9+
app = Flask(
10+
__name__, template_folder=".", static_folder=".", static_url_path="")
11+
app.config["SECRET_KEY"] = "secret!"
12+
app.config["fd"] = None
13+
app.config["child_pid"] = None
14+
socketio = SocketIO(app)
15+
16+
17+
def read_and_forward_pty_output():
18+
max_read_bytes = 1024 * 20
19+
while True:
20+
socketio.sleep(0.01)
21+
if app.config["fd"]:
22+
timeout_sec = 0
23+
(data_ready, _, _) = select.select([app.config["fd"]], [], [],
24+
timeout_sec)
25+
if data_ready:
26+
output = os.read(app.config["fd"], max_read_bytes).decode()
27+
socketio.emit(
28+
"pty-output", {"output": output}, namespace="/pty")
29+
30+
31+
@app.route("/")
32+
def index():
33+
return render_template("index.html")
34+
35+
36+
@socketio.on("pty-input", namespace="/pty")
37+
def pty_input(data):
38+
"""write to the child pty"""
39+
if app.config["fd"]:
40+
os.write(app.config["fd"], data["input"].encode())
41+
42+
43+
@socketio.on("connect", namespace="/pty")
44+
def connect():
45+
"""new client connected"""
46+
47+
if app.config["child_pid"]:
48+
# already started child process, don't start another
49+
return
50+
51+
# create child process attached to a pty we can read from and write to
52+
(child_pid, fd) = pty.fork()
53+
if child_pid == 0:
54+
# this is the child process fork.
55+
# anything printed here will show up in the pty, including the output
56+
# of this subprocess
57+
subprocess.run(["bash"])
58+
else:
59+
# this is the parent process fork.
60+
# store child fd and pid
61+
app.config["fd"] = fd
62+
app.config["child_pid"] = child_pid
63+
print("child pid is", child_pid)
64+
print("starting background task to continously read and forward pty "
65+
"output to client")
66+
socketio.start_background_task(target=read_and_forward_pty_output)
67+
print("task started")
68+
69+
70+
def main(debug=False, port=5000):
71+
print(f"serving on http://127.0.0.1:{port}")
72+
socketio.run(app, debug=debug, port=port)
73+
74+
75+
if __name__ == "__main__":
76+
main(debug=True)

pyxtermjs/index.html

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<html lang="en">
2+
<head>
3+
<meta charset="utf-8">
4+
<title>pyxterm.js</title>
5+
<style>
6+
html {
7+
font-family: arial;
8+
}
9+
</style>
10+
</head>
11+
<body>
12+
13+
<span style="font-size: 1.4em;">pyxterm.js</span>&nbsp;&nbsp;&nbsp;
14+
<span style="font-size: small;">status: <span style="font-size: small;" id="status">connecting...</span></span>
15+
16+
<div id="terminal"></div>
17+
<p style="text-align: right; font-size: small;">
18+
built by <a href="https://grassfedcode.com">Chad Smith</a> <a href="https://github.com/cs01">GitHub</a>
19+
</p>
20+
<!-- xterm -->
21+
<script src="https://unpkg.com/[email protected]/dist/xterm.js"></script>
22+
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/xterm.css" />
23+
24+
<!-- optional xterm add ons -->
25+
<!-- <script src="https://unpkg.com/[email protected]/dist/addons/fit/fit.js"></script>
26+
<script src="https://unpkg.com/[email protected]/dist/addons/fullscreen/fullscreen.js"></script>
27+
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/addons/fullscreen/fullscreen.css" /> -->
28+
29+
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script>
30+
31+
<script>
32+
// Terminal.applyAddon(fullscreen)
33+
// Terminal.applyAddon(fit)
34+
const term = new Terminal();
35+
term.open(document.getElementById('terminal'));
36+
37+
// term.toggleFullScreen(true)
38+
// term.fit()
39+
40+
term.on('key', (key, ev) => {
41+
console.log("pressed key", key)
42+
console.log("event", ev)
43+
socket.emit("pty-input", {"input": key})
44+
});
45+
46+
const socket = io.connect('/pty');
47+
const status = document.getElementById("status")
48+
49+
socket.on("pty-output", function(data){
50+
console.log("new output", data)
51+
term.write(data.output)
52+
})
53+
54+
socket.on("connect", () => {
55+
status.innerHTML = '<span style="background-color: lightgreen;">connected</span>'
56+
}
57+
)
58+
59+
socket.on("disconnect", () => {
60+
status.innerHTML = '<span style="background-color: #ff8383;">disconnected</span>'
61+
})
62+
63+
</script>
64+
</body>
65+
</html>

0 commit comments

Comments
 (0)