ar08 commited on
Commit
7c5f4e7
·
verified ·
1 Parent(s): 8d1c91e

Upload 4 files

Browse files
pyxtermjs/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ __version__ = "0.1.0"
pyxtermjs/__main__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from .app import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ exit(main())
pyxtermjs/app.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ from flask import Flask, render_template
4
+ from flask_socketio import SocketIO
5
+ import pty
6
+ import os
7
+ import subprocess
8
+ import select
9
+ import termios
10
+ import struct
11
+ import fcntl
12
+ import shlex
13
+ import logging
14
+ import sys
15
+
16
+ logging.getLogger("werkzeug").setLevel(logging.ERROR)
17
+
18
+ __version__ = "0.5.0.2"
19
+
20
+ app = Flask(__name__, template_folder=".", static_folder=".", static_url_path="")
21
+ app.config["SECRET_KEY"] = "secret!"
22
+ app.config["fd"] = None
23
+ app.config["child_pid"] = None
24
+ socketio = SocketIO(app)
25
+
26
+
27
+ def set_winsize(fd, row, col, xpix=0, ypix=0):
28
+ logging.debug("setting window size with termios")
29
+ winsize = struct.pack("HHHH", row, col, xpix, ypix)
30
+ fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
31
+
32
+
33
+ def read_and_forward_pty_output():
34
+ max_read_bytes = 1024 * 20
35
+ while True:
36
+ socketio.sleep(0.01)
37
+ if app.config["fd"]:
38
+ timeout_sec = 0
39
+ (data_ready, _, _) = select.select([app.config["fd"]], [], [], timeout_sec)
40
+ if data_ready:
41
+ output = os.read(app.config["fd"], max_read_bytes).decode(
42
+ errors="ignore"
43
+ )
44
+ socketio.emit("pty-output", {"output": output}, namespace="/pty")
45
+
46
+
47
+ @app.route("/")
48
+ def index():
49
+ return render_template("index.html")
50
+
51
+
52
+ @socketio.on("pty-input", namespace="/pty")
53
+ def pty_input(data):
54
+ """write to the child pty. The pty sees this as if you are typing in a real
55
+ terminal.
56
+ """
57
+ if app.config["fd"]:
58
+ logging.debug("received input from browser: %s" % data["input"])
59
+ os.write(app.config["fd"], data["input"].encode())
60
+
61
+
62
+ @socketio.on("resize", namespace="/pty")
63
+ def resize(data):
64
+ if app.config["fd"]:
65
+ logging.debug(f"Resizing window to {data['rows']}x{data['cols']}")
66
+ set_winsize(app.config["fd"], data["rows"], data["cols"])
67
+
68
+
69
+ @socketio.on("connect", namespace="/pty")
70
+ def connect():
71
+ """new client connected"""
72
+ logging.info("new client connected")
73
+ if app.config["child_pid"]:
74
+ # already started child process, don't start another
75
+ return
76
+
77
+ # create child process attached to a pty we can read from and write to
78
+ (child_pid, fd) = pty.fork()
79
+ if child_pid == 0:
80
+ # this is the child process fork.
81
+ # anything printed here will show up in the pty, including the output
82
+ # of this subprocess
83
+ subprocess.run(app.config["cmd"])
84
+ else:
85
+ # this is the parent process fork.
86
+ # store child fd and pid
87
+ app.config["fd"] = fd
88
+ app.config["child_pid"] = child_pid
89
+ set_winsize(fd, 50, 50)
90
+ cmd = " ".join(shlex.quote(c) for c in app.config["cmd"])
91
+ # logging/print statements must go after this because... I have no idea why
92
+ # but if they come before the background task never starts
93
+ socketio.start_background_task(target=read_and_forward_pty_output)
94
+
95
+ logging.info("child pid is " + child_pid)
96
+ logging.info(
97
+ f"starting background task with command `{cmd}` to continously read "
98
+ "and forward pty output to client"
99
+ )
100
+ logging.info("task started")
101
+
102
+
103
+ def main():
104
+ parser = argparse.ArgumentParser(
105
+ description=(
106
+ "A fully functional terminal in your browser. "
107
+ "https://github.com/cs01/pyxterm.js"
108
+ ),
109
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
110
+ )
111
+ parser.add_argument(
112
+ "-p", "--port", default=5000, help="port to run server on", type=int
113
+ )
114
+ parser.add_argument(
115
+ "--host",
116
+ default="127.0.0.1",
117
+ help="host to run server on (use 0.0.0.0 to allow access from other hosts)",
118
+ )
119
+ parser.add_argument("--debug", action="store_true", help="debug the server")
120
+ parser.add_argument("--version", action="store_true", help="print version and exit")
121
+ parser.add_argument(
122
+ "--command", default="bash", help="Command to run in the terminal"
123
+ )
124
+ parser.add_argument(
125
+ "--cmd-args",
126
+ default="",
127
+ help="arguments to pass to command (i.e. --cmd-args='arg1 arg2 --flag')",
128
+ )
129
+ args = parser.parse_args()
130
+ if args.version:
131
+ print(__version__)
132
+ exit(0)
133
+ app.config["cmd"] = [args.command] + shlex.split(args.cmd_args)
134
+ green = "\033[92m"
135
+ end = "\033[0m"
136
+ log_format = (
137
+ green
138
+ + "pyxtermjs > "
139
+ + end
140
+ + "%(levelname)s (%(funcName)s:%(lineno)s) %(message)s"
141
+ )
142
+ logging.basicConfig(
143
+ format=log_format,
144
+ stream=sys.stdout,
145
+ level=logging.DEBUG if args.debug else logging.INFO,
146
+ )
147
+ logging.info(f"serving on http://{args.host}:{args.port}")
148
+ socketio.run(app, debug=args.debug, port=args.port, host=args.host)
149
+
150
+
151
+ if __name__ == "__main__":
152
+ main()
pyxtermjs/index.html ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <html lang="en">
2
+ <head>
3
+ <meta charset="utf-8" />
4
+
5
+ <style>
6
+ html {
7
+ font-family: arial;
8
+ }
9
+ body{
10
+ background-color: black; /* Set background color to black */
11
+ color: white; /* Set text color to white */
12
+ margin: 0; /* Remove default margin */
13
+ padding: 0;
14
+ }
15
+ </style>
16
+ <link
17
+ rel="stylesheet"
18
+ href="https://unpkg.com/xterm@4.11.0/css/xterm.css"
19
+ />
20
+ </head>
21
+ <body>
22
+
23
+
24
+ <span style="font-size: small"
25
+ >status:
26
+ <span style="font-size: small" id="status">connecting...</span></span
27
+ >
28
+
29
+ <div style="width: 100%; height: calc(100% - 50px)" id="terminal"></div>
30
+
31
+ <p style="text-align: right; font-size: small">
32
+ Get Rickrolled <a href="https://www.youtube.com/watch?v=o-YBDTqX_ZU"></a>
33
+ <a href="https://www.youtube.com/watch?v=o-YBDTqX_ZU">GitHub</a>
34
+ </p>
35
+ <!-- xterm -->
36
+ <script src="https://unpkg.com/xterm@4.11.0/lib/xterm.js"></script>
37
+ <script src="https://unpkg.com/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script>
38
+ <script src="https://unpkg.com/xterm-addon-web-links@0.4.0/lib/xterm-addon-web-links.js"></script>
39
+ <script src="https://unpkg.com/xterm-addon-search@0.8.0/lib/xterm-addon-sear
40
+ ch.js"></script>
41
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script>
42
+
43
+ <script>
44
+ const term = new Terminal({
45
+ cursorBlink: true,
46
+ macOptionIsMeta: true,
47
+ scrollback: true,
48
+ });
49
+ term.attachCustomKeyEventHandler(customKeyEventHandler);
50
+ //
51
+ const fit = new FitAddon.FitAddon();
52
+ term.loadAddon(fit);
53
+ term.loadAddon(new WebLinksAddon.WebLinksAddon());
54
+ term.loadAddon(new SearchAddon.SearchAddon());
55
+
56
+ term.open(document.getElementById("terminal"));
57
+ fit.fit();
58
+ term.resize(15, 50);
59
+ console.log(`size: ${term.cols} columns, ${term.rows} rows`);
60
+ fit.fit();
61
+ term.writeln("Welcome to AR-server");
62
+ term.writeln("");
63
+ term.writeln('')
64
+ term.writeln("You can copy with ctrl+shift+x");
65
+ term.writeln("You can paste with ctrl+shift+v");
66
+ term.writeln('')
67
+ term.onData((data) => {
68
+ console.log("browser terminal received new data:", data);
69
+ socket.emit("pty-input", { input: data });
70
+ });
71
+
72
+ const socket = io.connect("/pty");
73
+ const status = document.getElementById("status");
74
+
75
+ socket.on("pty-output", function (data) {
76
+ console.log("new output received from server:", data.output);
77
+ term.write(data.output);
78
+ });
79
+
80
+ socket.on("connect", () => {
81
+ fitToscreen();
82
+ status.innerHTML =
83
+ '<span style="background-color: lightgreen;">connected</span>';
84
+ });
85
+
86
+ socket.on("disconnect", () => {
87
+ status.innerHTML =
88
+ '<span style="background-color: #ff8383;">disconnected</span>';
89
+ });
90
+
91
+ function fitToscreen() {
92
+ fit.fit();
93
+ const dims = { cols: term.cols, rows: term.rows };
94
+ console.log("sending new dimensions to server's pty", dims);
95
+ socket.emit("resize", dims);
96
+ }
97
+
98
+ function debounce(func, wait_ms) {
99
+ let timeout;
100
+ return function (...args) {
101
+ const context = this;
102
+ clearTimeout(timeout);
103
+ timeout = setTimeout(() => func.apply(context, args), wait_ms);
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Handle copy and paste events
109
+ */
110
+ function customKeyEventHandler(e) {
111
+ if (e.type !== "keydown") {
112
+ return true;
113
+ }
114
+ if (e.ctrlKey && e.shiftKey) {
115
+ const key = e.key.toLowerCase();
116
+ if (key === "v") {
117
+ // ctrl+shift+v: paste whatever is in the clipboard
118
+ navigator.clipboard.readText().then((toPaste) => {
119
+ term.writeText(toPaste);
120
+ });
121
+ return false;
122
+ } else if (key === "c" || key === "x") {
123
+ // ctrl+shift+x: copy whatever is highlighted to clipboard
124
+
125
+ // 'x' is used as an alternate to 'c' because ctrl+c is taken
126
+ // by the terminal (SIGINT) and ctrl+shift+c is taken by the browser
127
+ // (open devtools).
128
+ // I'm not aware of ctrl+shift+x being used by anything in the terminal
129
+ // or browser
130
+ const toCopy = term.getSelection();
131
+ navigator.clipboard.writeText(toCopy);
132
+ term.focus();
133
+ return false;
134
+ }
135
+ }
136
+ return true;
137
+ }
138
+
139
+ const wait_ms = 50;
140
+ window.onresize = debounce(fitToscreen, wait_ms);
141
+ </script>
142
+ </body>
143
+ </html>