@@ -14,31 +14,73 @@ const notifyAndThrow = (message) => {
14
14
throw new Error ( message ) ;
15
15
} ;
16
16
17
+ const notParsedYet = ( script ) => ! bootstrapped . has ( script ) ;
18
+
19
+ const onceOnMain = ( { attributes : { worker } } ) => ! worker ;
20
+
21
+ const bootstrapped = new WeakSet ( ) ;
22
+
23
+ let addStyle = true ;
24
+
25
+ // this callback will be serialized as string and it never needs
26
+ // to be invoked multiple times. Each xworker here is bootstrapped
27
+ // only once thanks to the `sync.is_pyterminal()` check.
28
+ const workerReady = ( { interpreter, io, run } , { sync } ) => {
29
+ if ( ! sync . is_pyterminal ( ) ) return ;
30
+
31
+ // in workers it's always safe to grab the polyscript currentScript
32
+ run ( "from polyscript.currentScript import terminal as __terminal__" ) ;
33
+
34
+ // This part is inevitably duplicated as external scope
35
+ // can't be reached by workers out of the box.
36
+ // The detail is that here we use sync though, not readline.
37
+ const decoder = new TextDecoder ( ) ;
38
+ let data = "" ;
39
+ const generic = {
40
+ isatty : true ,
41
+ write ( buffer ) {
42
+ data = decoder . decode ( buffer ) ;
43
+ sync . pyterminal_write ( data ) ;
44
+ return buffer . length ;
45
+ } ,
46
+ } ;
47
+ interpreter . setStdout ( generic ) ;
48
+ interpreter . setStderr ( generic ) ;
49
+ interpreter . setStdin ( {
50
+ isatty : true ,
51
+ stdin : ( ) => sync . pyterminal_read ( data ) ,
52
+ } ) ;
53
+
54
+ io . stderr = ( error ) => {
55
+ sync . pyterminal_write ( `${ error . message || error } \n` ) ;
56
+ } ;
57
+ } ;
58
+
17
59
const pyTerminal = async ( ) => {
18
60
const terminals = document . querySelectorAll ( SELECTOR ) ;
19
61
20
- // no results will look further for runtime nodes
21
- if ( ! terminals . length ) return ;
62
+ const unknown = [ ] . filter . call ( terminals , notParsedYet ) ;
22
63
23
- // if we arrived this far, let's drop the MutationObserver
24
- // as we only support one terminal per page (right now).
25
- mo . disconnect ( ) ;
64
+ // no results will look further for runtime nodes
65
+ if ( ! unknown . length ) return ;
66
+ // early flag elements as known to avoid concurrent
67
+ // MutationObserver invokes of this async handler
68
+ else unknown . forEach ( bootstrapped . add , bootstrapped ) ;
26
69
27
70
// we currently support only one terminal as in "classic"
28
- if ( terminals . length > 1 ) notifyAndThrow ( "You can use at most 1 terminal." ) ;
29
-
30
- const [ element ] = terminals ;
31
- // hopefully to be removed in the near future!
32
- if ( element . matches ( 'script[type="mpy"],mpy-script' ) )
33
- notifyAndThrow ( "Unsupported terminal." ) ;
71
+ if ( [ ] . filter . call ( terminals , onceOnMain ) . length > 1 )
72
+ notifyAndThrow ( "You can use at most 1 main terminal" ) ;
34
73
35
74
// import styles lazily
36
- document . head . append (
37
- Object . assign ( document . createElement ( "link" ) , {
38
- rel : "stylesheet" ,
39
- href : new URL ( "./xterm.css" , import . meta. url ) ,
40
- } ) ,
41
- ) ;
75
+ if ( addStyle ) {
76
+ addStyle = false ;
77
+ document . head . append (
78
+ Object . assign ( document . createElement ( "link" ) , {
79
+ rel : "stylesheet" ,
80
+ href : new URL ( "./xterm.css" , import . meta. url ) ,
81
+ } ) ,
82
+ ) ;
83
+ }
42
84
43
85
// lazy load these only when a valid terminal is found
44
86
const [ { Terminal } , { Readline } , { FitAddon } ] = await Promise . all ( [
@@ -47,136 +89,113 @@ const pyTerminal = async () => {
47
89
import ( /* webpackIgnore: true */ "../3rd-party/xterm_addon-fit.js" ) ,
48
90
] ) ;
49
91
50
- const readline = new Readline ( ) ;
51
-
52
- // common main thread initialization for both worker
53
- // or main case, bootstrapping the terminal on its target
54
- const init = ( options ) => {
55
- let target = element ;
56
- const selector = element . getAttribute ( "target" ) ;
57
- if ( selector ) {
58
- target =
59
- document . getElementById ( selector ) ||
60
- document . querySelector ( selector ) ;
61
- if ( ! target ) throw new Error ( `Unknown target ${ selector } ` ) ;
62
- } else {
63
- target = document . createElement ( "py-terminal" ) ;
64
- target . style . display = "block" ;
65
- element . after ( target ) ;
66
- }
67
- const terminal = new Terminal ( {
68
- theme : {
69
- background : "#191A19" ,
70
- foreground : "#F5F2E7" ,
71
- } ,
72
- ...options ,
73
- } ) ;
74
- const fitAddon = new FitAddon ( ) ;
75
- terminal . loadAddon ( fitAddon ) ;
76
- terminal . loadAddon ( readline ) ;
77
- terminal . open ( target ) ;
78
- fitAddon . fit ( ) ;
79
- terminal . focus ( ) ;
80
- defineProperty ( element , "terminal" , { value : terminal } ) ;
81
- return terminal ;
82
- } ;
83
-
84
- // branch logic for the worker
85
- if ( element . hasAttribute ( "worker" ) ) {
86
- // when the remote thread onReady triggers:
87
- // setup the interpreter stdout and stderr
88
- const workerReady = ( { interpreter, io, run } , { sync } ) => {
89
- // in workers it's always safe to grab the polyscript currentScript
90
- run (
91
- "from polyscript.currentScript import terminal as __terminal__" ,
92
- ) ;
93
- sync . pyterminal_drop_hooks ( ) ;
94
-
95
- // This part is inevitably duplicated as external scope
96
- // can't be reached by workers out of the box.
97
- // The detail is that here we use sync though, not readline.
98
- const decoder = new TextDecoder ( ) ;
99
- let data = "" ;
100
- const generic = {
101
- isatty : true ,
102
- write ( buffer ) {
103
- data = decoder . decode ( buffer ) ;
104
- sync . pyterminal_write ( data ) ;
105
- return buffer . length ;
92
+ for ( const element of unknown ) {
93
+ // hopefully to be removed in the near future!
94
+ if ( element . matches ( 'script[type="mpy"],mpy-script' ) )
95
+ notifyAndThrow ( "Unsupported terminal." ) ;
96
+
97
+ const readline = new Readline ( ) ;
98
+
99
+ // common main thread initialization for both worker
100
+ // or main case, bootstrapping the terminal on its target
101
+ const init = ( options ) => {
102
+ let target = element ;
103
+ const selector = element . getAttribute ( "target" ) ;
104
+ if ( selector ) {
105
+ target =
106
+ document . getElementById ( selector ) ||
107
+ document . querySelector ( selector ) ;
108
+ if ( ! target ) throw new Error ( `Unknown target ${ selector } ` ) ;
109
+ } else {
110
+ target = document . createElement ( "py-terminal" ) ;
111
+ target . style . display = "block" ;
112
+ element . after ( target ) ;
113
+ }
114
+ const terminal = new Terminal ( {
115
+ theme : {
116
+ background : "#191A19" ,
117
+ foreground : "#F5F2E7" ,
106
118
} ,
107
- } ;
108
- interpreter . setStdout ( generic ) ;
109
- interpreter . setStderr ( generic ) ;
110
- interpreter . setStdin ( {
111
- isatty : true ,
112
- stdin : ( ) => sync . pyterminal_read ( data ) ,
119
+ ...options ,
113
120
} ) ;
114
-
115
- io . stderr = ( error ) => {
116
- sync . pyterminal_write ( `${ error . message || error } \n` ) ;
117
- } ;
121
+ const fitAddon = new FitAddon ( ) ;
122
+ terminal . loadAddon ( fitAddon ) ;
123
+ terminal . loadAddon ( readline ) ;
124
+ terminal . open ( target ) ;
125
+ fitAddon . fit ( ) ;
126
+ terminal . focus ( ) ;
127
+ defineProperty ( element , "terminal" , { value : terminal } ) ;
128
+ return terminal ;
118
129
} ;
119
130
120
- // add a hook on the main thread to setup all sync helpers
121
- // also bootstrapping the XTerm target on main
122
- hooks . main . onWorker . add ( function worker ( _ , xworker ) {
123
- hooks . main . onWorker . delete ( worker ) ;
124
- init ( {
125
- disableStdin : false ,
126
- cursorBlink : true ,
127
- cursorStyle : "block" ,
128
- } ) ;
129
- xworker . sync . pyterminal_read = readline . read . bind ( readline ) ;
130
- xworker . sync . pyterminal_write = readline . write . bind ( readline ) ;
131
- // allow a worker to drop main thread hooks ASAP
132
- xworker . sync . pyterminal_drop_hooks = ( ) => {
133
- hooks . worker . onReady . delete ( workerReady ) ;
134
- } ;
135
- } ) ;
136
-
137
- // setup remote thread JS/Python code for whenever the
138
- // worker is ready to become a terminal
139
- hooks . worker . onReady . add ( workerReady ) ;
140
- } else {
141
- // in the main case, just bootstrap XTerm without
142
- // allowing any input as that's not possible / awkward
143
- hooks . main . onReady . add ( function main ( { interpreter, io, run } ) {
144
- console . warn ( "py-terminal is read only on main thread" ) ;
145
- hooks . main . onReady . delete ( main ) ;
146
-
147
- // on main, it's easy to trash and clean the current terminal
148
- globalThis . __py_terminal__ = init ( {
149
- disableStdin : true ,
150
- cursorBlink : false ,
151
- cursorStyle : "underline" ,
152
- } ) ;
153
- run ( "from js import __py_terminal__ as __terminal__" ) ;
154
- delete globalThis . __py_terminal__ ;
155
-
156
- // This part is inevitably duplicated as external scope
157
- // can't be reached by workers out of the box.
158
- // The detail is that here we use readline here, not sync.
159
- const decoder = new TextDecoder ( ) ;
160
- let data = "" ;
161
- const generic = {
162
- isatty : true ,
163
- write ( buffer ) {
164
- data = decoder . decode ( buffer ) ;
165
- readline . write ( data ) ;
166
- return buffer . length ;
167
- } ,
168
- } ;
169
- interpreter . setStdout ( generic ) ;
170
- interpreter . setStderr ( generic ) ;
171
- interpreter . setStdin ( {
172
- isatty : true ,
173
- stdin : ( ) => readline . read ( data ) ,
131
+ // branch logic for the worker
132
+ if ( element . hasAttribute ( "worker" ) ) {
133
+ // add a hook on the main thread to setup all sync helpers
134
+ // also bootstrapping the XTerm target on main *BUT* ...
135
+ hooks . main . onWorker . add ( function worker ( _ , xworker ) {
136
+ // ... as multiple workers will add multiple callbacks
137
+ // be sure no xworker is ever initialized twice!
138
+ if ( bootstrapped . has ( xworker ) ) return ;
139
+ bootstrapped . add ( xworker ) ;
140
+
141
+ // still cleanup this callback for future scripts/workers
142
+ hooks . main . onWorker . delete ( worker ) ;
143
+
144
+ init ( {
145
+ disableStdin : false ,
146
+ cursorBlink : true ,
147
+ cursorStyle : "block" ,
148
+ } ) ;
149
+
150
+ xworker . sync . is_pyterminal = ( ) => true ;
151
+ xworker . sync . pyterminal_read = readline . read . bind ( readline ) ;
152
+ xworker . sync . pyterminal_write = readline . write . bind ( readline ) ;
174
153
} ) ;
175
154
176
- io . stderr = ( error ) => {
177
- readline . write ( `${ error . message || error } \n` ) ;
178
- } ;
179
- } ) ;
155
+ // setup remote thread JS/Python code for whenever the
156
+ // worker is ready to become a terminal
157
+ hooks . worker . onReady . add ( workerReady ) ;
158
+ } else {
159
+ // in the main case, just bootstrap XTerm without
160
+ // allowing any input as that's not possible / awkward
161
+ hooks . main . onReady . add ( function main ( { interpreter, io, run } ) {
162
+ console . warn ( "py-terminal is read only on main thread" ) ;
163
+ hooks . main . onReady . delete ( main ) ;
164
+
165
+ // on main, it's easy to trash and clean the current terminal
166
+ globalThis . __py_terminal__ = init ( {
167
+ disableStdin : true ,
168
+ cursorBlink : false ,
169
+ cursorStyle : "underline" ,
170
+ } ) ;
171
+ run ( "from js import __py_terminal__ as __terminal__" ) ;
172
+ delete globalThis . __py_terminal__ ;
173
+
174
+ // This part is inevitably duplicated as external scope
175
+ // can't be reached by workers out of the box.
176
+ // The detail is that here we use readline here, not sync.
177
+ const decoder = new TextDecoder ( ) ;
178
+ let data = "" ;
179
+ const generic = {
180
+ isatty : true ,
181
+ write ( buffer ) {
182
+ data = decoder . decode ( buffer ) ;
183
+ readline . write ( data ) ;
184
+ return buffer . length ;
185
+ } ,
186
+ } ;
187
+ interpreter . setStdout ( generic ) ;
188
+ interpreter . setStderr ( generic ) ;
189
+ interpreter . setStdin ( {
190
+ isatty : true ,
191
+ stdin : ( ) => readline . read ( data ) ,
192
+ } ) ;
193
+
194
+ io . stderr = ( error ) => {
195
+ readline . write ( `${ error . message || error } \n` ) ;
196
+ } ;
197
+ } ) ;
198
+ }
180
199
}
181
200
} ;
182
201
0 commit comments