1
1
// PyScript py-terminal plugin
2
- import { TYPES , hooks } from "../core.js" ;
2
+ import { TYPES } from "../core.js" ;
3
3
import { notify } from "./error.js" ;
4
- import { customObserver , defineProperties } from "polyscript/exports" ;
4
+ import { customObserver } from "polyscript/exports" ;
5
5
6
6
// will contain all valid selectors
7
7
const SELECTORS = [ ] ;
8
8
9
+ // avoid processing same elements twice
10
+ const processed = new WeakSet ( ) ;
11
+
9
12
// show the error on main and
10
13
// stops the module from keep executing
11
14
const notifyAndThrow = ( message ) => {
@@ -15,265 +18,10 @@ const notifyAndThrow = (message) => {
15
18
16
19
const onceOnMain = ( { attributes : { worker } } ) => ! worker ;
17
20
18
- const bootstrapped = new WeakSet ( ) ;
19
-
20
21
let addStyle = true ;
21
22
22
- // this callback will be serialized as string and it never needs
23
- // to be invoked multiple times. Each xworker here is bootstrapped
24
- // only once thanks to the `sync.is_pyterminal()` check.
25
- const workerReady = ( { interpreter, io, run, type } , { sync } ) => {
26
- if ( ! sync . is_pyterminal ( ) ) return ;
27
-
28
- // in workers it's always safe to grab the polyscript currentScript
29
- // the ugly `_` dance is due MicroPython not able to import via:
30
- // `from polyscript.currentScript import terminal as __terminal__`
31
- run (
32
- "from polyscript import currentScript as _; __terminal__ = _.terminal; del _" ,
33
- ) ;
34
-
35
- let data = "" ;
36
- const { pyterminal_read, pyterminal_write } = sync ;
37
- const decoder = new TextDecoder ( ) ;
38
- const generic = {
39
- isatty : false ,
40
- write ( buffer ) {
41
- data = decoder . decode ( buffer ) ;
42
- pyterminal_write ( data ) ;
43
- return buffer . length ;
44
- } ,
45
- } ;
46
-
47
- // This part works already in both Pyodide and MicroPython
48
- io . stderr = ( error ) => {
49
- pyterminal_write ( String ( error . message || error ) ) ;
50
- } ;
51
-
52
- // MicroPython has no code or code.interact()
53
- // This part patches it in a way that simulates
54
- // the code.interact() module in Pyodide.
55
- if ( type === "mpy" ) {
56
- // monkey patch global input otherwise broken in MicroPython
57
- interpreter . registerJsModule ( "_pyscript_input" , {
58
- input : pyterminal_read ,
59
- } ) ;
60
- run ( "from _pyscript_input import input" ) ;
61
-
62
- // this is needed to avoid truncated unicode in MicroPython
63
- // the reason is that `linebuffer` false just send one byte
64
- // per time and readline here doesn't like it much.
65
- // MicroPython also has issues with code-points and
66
- // replProcessChar(byte) but that function accepts only
67
- // one byte per time so ... we have an issue!
68
- // @see https://github.com/pyscript/pyscript/pull/2018
69
- // @see https://github.com/WebReflection/buffer-points
70
- const bufferPoints = ( stdio ) => {
71
- const bytes = [ ] ;
72
- let needed = 0 ;
73
- return ( buffer ) => {
74
- let written = 0 ;
75
- for ( const byte of buffer ) {
76
- bytes . push ( byte ) ;
77
- // @see https://encoding.spec.whatwg.org/#utf-8-bytes-needed
78
- if ( needed ) needed -- ;
79
- else if ( 0xc2 <= byte && byte <= 0xdf ) needed = 1 ;
80
- else if ( 0xe0 <= byte && byte <= 0xef ) needed = 2 ;
81
- else if ( 0xf0 <= byte && byte <= 0xf4 ) needed = 3 ;
82
- if ( ! needed ) {
83
- written += bytes . length ;
84
- stdio ( new Uint8Array ( bytes . splice ( 0 ) ) ) ;
85
- }
86
- }
87
- return written ;
88
- } ;
89
- } ;
90
-
91
- io . stdout = bufferPoints ( generic . write ) ;
92
-
93
- // tiny shim of the code module with only interact
94
- // to bootstrap a REPL like environment
95
- interpreter . registerJsModule ( "code" , {
96
- interact ( ) {
97
- let input = "" ;
98
- let length = 1 ;
99
-
100
- const encoder = new TextEncoder ( ) ;
101
- const acc = [ ] ;
102
- const handlePoints = bufferPoints ( ( buffer ) => {
103
- acc . push ( ...buffer ) ;
104
- pyterminal_write ( decoder . decode ( buffer ) ) ;
105
- } ) ;
106
-
107
- // avoid duplicating the output produced by the input
108
- io . stdout = ( buffer ) =>
109
- length ++ > input . length ? handlePoints ( buffer ) : 0 ;
110
-
111
- interpreter . replInit ( ) ;
112
-
113
- // loop forever waiting for user inputs
114
- ( function repl ( ) {
115
- const out = decoder . decode ( new Uint8Array ( acc . splice ( 0 ) ) ) ;
116
- // print in current line only the last line produced by the REPL
117
- const data = `${ pyterminal_read ( out . split ( "\n" ) . at ( - 1 ) ) } \r` ;
118
- length = 0 ;
119
- input = encoder . encode ( data ) ;
120
- for ( const c of input ) interpreter . replProcessChar ( c ) ;
121
- repl ( ) ;
122
- } ) ( ) ;
123
- } ,
124
- } ) ;
125
- } else {
126
- interpreter . setStdout ( generic ) ;
127
- interpreter . setStderr ( generic ) ;
128
- interpreter . setStdin ( {
129
- isatty : false ,
130
- stdin : ( ) => pyterminal_read ( data ) ,
131
- } ) ;
132
- }
133
- } ;
134
-
135
- const pyTerminal = async ( element ) => {
136
- // lazy load these only when a valid terminal is found
137
- const [ { Terminal } , { Readline } , { FitAddon } , { WebLinksAddon } ] =
138
- await Promise . all ( [
139
- import ( /* webpackIgnore: true */ "../3rd-party/xterm.js" ) ,
140
- import ( /* webpackIgnore: true */ "../3rd-party/xterm-readline.js" ) ,
141
- import ( /* webpackIgnore: true */ "../3rd-party/xterm_addon-fit.js" ) ,
142
- import (
143
- /* webpackIgnore: true */ "../3rd-party/xterm_addon-web-links.js"
144
- ) ,
145
- ] ) ;
146
-
147
- const readline = new Readline ( ) ;
148
-
149
- // common main thread initialization for both worker
150
- // or main case, bootstrapping the terminal on its target
151
- const init = ( options ) => {
152
- let target = element ;
153
- const selector = element . getAttribute ( "target" ) ;
154
- if ( selector ) {
155
- target =
156
- document . getElementById ( selector ) ||
157
- document . querySelector ( selector ) ;
158
- if ( ! target ) throw new Error ( `Unknown target ${ selector } ` ) ;
159
- } else {
160
- target = document . createElement ( "py-terminal" ) ;
161
- target . style . display = "block" ;
162
- element . after ( target ) ;
163
- }
164
- const terminal = new Terminal ( {
165
- theme : {
166
- background : "#191A19" ,
167
- foreground : "#F5F2E7" ,
168
- } ,
169
- ...options ,
170
- } ) ;
171
- const fitAddon = new FitAddon ( ) ;
172
- terminal . loadAddon ( fitAddon ) ;
173
- terminal . loadAddon ( readline ) ;
174
- terminal . loadAddon ( new WebLinksAddon ( ) ) ;
175
- terminal . open ( target ) ;
176
- fitAddon . fit ( ) ;
177
- terminal . focus ( ) ;
178
- defineProperties ( element , {
179
- terminal : { value : terminal } ,
180
- process : {
181
- value : async ( code ) => {
182
- // this loop is the only way I could find to actually simulate
183
- // the user input char after char in a way that works in both
184
- // MicroPython and Pyodide
185
- for ( const line of code . split ( / (?: \r | \n | \r \n ) / ) ) {
186
- terminal . paste ( `${ line } \n` ) ;
187
- do {
188
- await new Promise ( ( resolve ) =>
189
- setTimeout ( resolve , 0 ) ,
190
- ) ;
191
- } while ( ! readline . activeRead ?. resolve ) ;
192
- readline . activeRead . resolve ( line ) ;
193
- }
194
- } ,
195
- } ,
196
- } ) ;
197
- return terminal ;
198
- } ;
199
-
200
- // branch logic for the worker
201
- if ( element . hasAttribute ( "worker" ) ) {
202
- // add a hook on the main thread to setup all sync helpers
203
- // also bootstrapping the XTerm target on main *BUT* ...
204
- hooks . main . onWorker . add ( function worker ( _ , xworker ) {
205
- // ... as multiple workers will add multiple callbacks
206
- // be sure no xworker is ever initialized twice!
207
- if ( bootstrapped . has ( xworker ) ) return ;
208
- bootstrapped . add ( xworker ) ;
209
-
210
- // still cleanup this callback for future scripts/workers
211
- hooks . main . onWorker . delete ( worker ) ;
212
-
213
- init ( {
214
- disableStdin : false ,
215
- cursorBlink : true ,
216
- cursorStyle : "block" ,
217
- } ) ;
218
-
219
- xworker . sync . is_pyterminal = ( ) => true ;
220
- xworker . sync . pyterminal_read = readline . read . bind ( readline ) ;
221
- xworker . sync . pyterminal_write = readline . write . bind ( readline ) ;
222
- } ) ;
223
-
224
- // setup remote thread JS/Python code for whenever the
225
- // worker is ready to become a terminal
226
- hooks . worker . onReady . add ( workerReady ) ;
227
- } else {
228
- // in the main case, just bootstrap XTerm without
229
- // allowing any input as that's not possible / awkward
230
- hooks . main . onReady . add ( function main ( { interpreter, io, run, type } ) {
231
- console . warn ( "py-terminal is read only on main thread" ) ;
232
- hooks . main . onReady . delete ( main ) ;
233
-
234
- // on main, it's easy to trash and clean the current terminal
235
- globalThis . __py_terminal__ = init ( {
236
- disableStdin : true ,
237
- cursorBlink : false ,
238
- cursorStyle : "underline" ,
239
- } ) ;
240
- run ( "from js import __py_terminal__ as __terminal__" ) ;
241
- delete globalThis . __py_terminal__ ;
242
-
243
- io . stderr = ( error ) => {
244
- readline . write ( String ( error . message || error ) ) ;
245
- } ;
246
-
247
- if ( type === "mpy" ) {
248
- interpreter . setStdin = Object ; // as no-op
249
- interpreter . setStderr = Object ; // as no-op
250
- interpreter . setStdout = ( { write } ) => {
251
- io . stdout = write ;
252
- } ;
253
- }
254
-
255
- let data = "" ;
256
- const decoder = new TextDecoder ( ) ;
257
- const generic = {
258
- isatty : false ,
259
- write ( buffer ) {
260
- data = decoder . decode ( buffer ) ;
261
- readline . write ( data ) ;
262
- return buffer . length ;
263
- } ,
264
- } ;
265
- interpreter . setStdout ( generic ) ;
266
- interpreter . setStderr ( generic ) ;
267
- interpreter . setStdin ( {
268
- isatty : false ,
269
- stdin : ( ) => readline . read ( data ) ,
270
- } ) ;
271
- } ) ;
272
- }
273
- } ;
274
-
275
- for ( const key of TYPES . keys ( ) ) {
276
- const selector = `script[type="${ key } "][terminal],${ key } -script[terminal]` ;
23
+ for ( const type of TYPES . keys ( ) ) {
24
+ const selector = `script[type="${ type } "][terminal],${ type } -script[terminal]` ;
277
25
SELECTORS . push ( selector ) ;
278
26
customObserver . set ( selector , async ( element ) => {
279
27
// we currently support only one terminal on main as in "classic"
@@ -292,6 +40,21 @@ for (const key of TYPES.keys()) {
292
40
) ;
293
41
}
294
42
295
- await pyTerminal ( element ) ;
43
+ if ( processed . has ( element ) ) return ;
44
+ processed . add ( element ) ;
45
+
46
+ const bootstrap = ( module ) => module . default ( element ) ;
47
+
48
+ // we can't be smart with template literals for the dynamic import
49
+ // or bundlers are incapable of producing multiple files around
50
+ if ( type === "mpy" ) {
51
+ await import ( /* webpackIgnore: true */ "./py-terminal/mpy.js" ) . then (
52
+ bootstrap ,
53
+ ) ;
54
+ } else {
55
+ await import ( /* webpackIgnore: true */ "./py-terminal/py.js" ) . then (
56
+ bootstrap ,
57
+ ) ;
58
+ }
296
59
} ) ;
297
60
}
0 commit comments