pyfrilet 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,8 +6,7 @@ Il combine [p5.js](https://p5js.org/) (dessin 2D) et [Pyodide](https://pyodide.o
6
6
  ---
7
7
 
8
8
  ## Démarrage rapide
9
-
10
- ```html
9
+ ````html
11
10
  <!doctype html>
12
11
  <html lang="fr">
13
12
  <head>
@@ -32,7 +31,7 @@ def draw():
32
31
  </script>
33
32
  </body>
34
33
  </html>
35
- ```
34
+ ````
36
35
 
37
36
  C'est tout. pyfrilet se charge de démarrer Pyodide, de monter le module `p5`, et de lancer le sketch. Les dépendances (p5.js, Pyodide, ACE) sont chargées depuis des CDN publics par défaut.
38
37
 
@@ -118,24 +117,23 @@ Ces fonctions sont définies par l'utilisateur et appelées automatiquement par
118
117
  | `noSmooth()` | Désactive l'antialiasing (pixel art). Affecte formes **et** texte. |
119
118
  | `sketchTitle(s)` | Affiche un texte dans la barre de contrôle. À appeler dans `setup()`. |
120
119
  | `getCanvas()` | Retourne le `p5.Element` wrappant le canvas. Utile pour appeler des méthodes p5.Element comme `.drop()`. À appeler dans `setup()` ou après. |
120
+ | `safe_proxy(fn)` | Wrape une fonction Python pour l'utiliser comme callback JS (équivalent de `create_proxy`) en capturant les erreurs et en les affichant dans le terminal. À préférer à `create_proxy` pour les callbacks utilisateur (`.drop()`, événements DOM…). |
121
121
 
122
122
  #### Propriétés dynamiques
123
123
 
124
124
  Ces variables sont mises à jour automatiquement avant chaque appel à `draw()`, `keyPressed()` et `mousePressed()`, y compris si elles ont été importées avec `from p5 import *` :
125
-
126
- ```python
125
+ ````python
127
126
  mouseX, mouseY # position de la souris dans le repère du canvas
128
127
  width, height # dimensions logiques du canvas
129
128
  frameCount # numéro de la frame courante
130
129
  key # dernière touche appuyée (caractère)
131
130
  keyCode # code numérique de la dernière touche
132
- ```
131
+ ````
133
132
 
134
133
  > **Note sur `keyCode`** : p5.js expose des constantes nommées (`LEFT_ARROW`, `RIGHT_ARROW`, `UP_ARROW`, `DOWN_ARROW`, `ENTER`, `BACKSPACE`…) directement importables avec `from p5 import *`.
135
134
 
136
135
  ### Exemple : plein écran réactif
137
-
138
- ```python
136
+ ````python
139
137
  from p5 import *
140
138
 
141
139
  def setup():
@@ -151,11 +149,10 @@ def draw():
151
149
  noStroke()
152
150
  fill('#7aa2f7')
153
151
  circle(width / 2, height / 2, min(width, height) * 0.4)
154
- ```
152
+ ````
155
153
 
156
154
  ### Exemple : navigation clavier
157
-
158
- ```python
155
+ ````python
159
156
  from p5 import *
160
157
 
161
158
  page = 0
@@ -174,13 +171,12 @@ def draw():
174
171
  fill('#e0af68')
175
172
  textSize(16)
176
173
  text("page " + str(page), 160, 150)
177
- ```
174
+ ````
178
175
 
179
176
  ### Packages Python tiers
180
177
 
181
178
  pyfrilet détecte automatiquement les imports du sketch et charge les packages disponibles dans la distribution Pyodide avant l'exécution. Il n'y a rien à faire :
182
-
183
- ```python
179
+ ````python
184
180
  from p5 import *
185
181
  import numpy as np # chargé automatiquement
186
182
  import networkx as nx # chargé automatiquement
@@ -190,23 +186,23 @@ def setup():
190
186
 
191
187
  def draw():
192
188
  background(20)
193
- ```
189
+ ````
194
190
 
195
191
  Un message "Chargement des dépendances…" s'affiche pendant le téléchargement. Seuls les packages inclus dans la [distribution Pyodide](https://pyodide.org/en/stable/usage/packages-in-pyodide.html) sont supportés (numpy, scipy, pandas, networkx, pillow…). Les packages pip arbitraires ne sont pas supportés.
196
192
 
197
- ### Glisser-déposer de fichiers
193
+ > **`rich` et `pygments`** sont chargés d'office au démarrage — ils alimentent l'affichage coloré des erreurs et sont disponibles dans vos sketches sans import supplémentaire.
198
194
 
199
- Pour recevoir des fichiers glissés sur le canvas, on utilise `getCanvas().drop()` avec `create_proxy` de Pyodide :
195
+ ### Glisser-déposer de fichiers
200
196
 
201
- ```python
197
+ Pour recevoir des fichiers glissés sur le canvas, on utilise `getCanvas().drop()` avec `safe_proxy` :
198
+ ````python
202
199
  from p5 import *
203
- from pyodide.ffi import create_proxy
204
200
 
205
201
  img = None
206
202
 
207
203
  def setup():
208
204
  size(400, 400)
209
- getCanvas().drop(create_proxy(on_drop))
205
+ getCanvas().drop(safe_proxy(on_drop))
210
206
 
211
207
  def on_drop(file):
212
208
  global img
@@ -221,21 +217,99 @@ def draw():
221
217
  background(40)
222
218
  if img:
223
219
  image(img, 0, 0, width, height)
224
- ```
220
+ ````
221
+
222
+ `safe_proxy` est préférable à `create_proxy` (de `pyodide.ffi`) pour les callbacks utilisateur : les erreurs qui surviennent dans le callback sont capturées et affichées proprement dans le terminal au lieu de disparaître silencieusement dans la console du navigateur.
225
223
 
226
224
  ---
227
225
 
228
226
  ### Note sur `smooth()` / `noSmooth()` et le texte
229
227
 
230
228
  Par défaut le canvas est rendu en mode antialiasé. `noSmooth()` bascule en mode pixel art — formes **et** texte sont pixelisés. Pour mélanger les deux dans le même `draw()` :
231
-
232
- ```python
229
+ ````python
233
230
  noSmooth()
234
231
  rect(10, 10, 80, 80) # bords nets
235
232
  smooth()
236
233
  textSize(14)
237
234
  text("lisible", 10, 120) # texte antialiasé
238
- ```
235
+ ````
236
+
237
+ ---
238
+
239
+ ### Gestion des erreurs
240
+
241
+ Quelle que soit l'origine de l'erreur (mode p5 ou mode terminal), pyfrilet affiche un traceback formaté par [rich](https://rich.readthedocs.io/) dans le terminal intégré : cadre coloré, fichier et numéro de ligne, extrait du code source. Les frames internes de pyfrilet sont automatiquement filtrées — seul le code utilisateur apparaît.
242
+
243
+ En mode p5, le sketch est arrêté dès la première erreur et le traceback s'affiche en overlay semi-transparent par-dessus le canvas. Corriger le code et relancer suffit à reprendre.
244
+
245
+ ---
246
+
247
+ ## Mode terminal
248
+
249
+ Quand le code Python ne contient **aucun import de `p5`**, pyfrilet bascule automatiquement en **mode terminal** : le canvas est remplacé par un terminal [xterm.js](https://xtermjs.org/) qui reçoit `stdout` et `stderr`.
250
+
251
+ Ce mode permet d'écrire des programmes Python classiques — algorithmes, structures de données, visualisations texte — sans aucun lien avec p5.
252
+ ````python
253
+ # Pas d'import p5 → mode terminal automatique
254
+ import asyncio
255
+
256
+ for i in range(5):
257
+ print(f"étape {i + 1}")
258
+ await asyncio.sleep(0.5)
259
+
260
+ print("terminé !")
261
+ ````
262
+
263
+ ### `input()`
264
+
265
+ `input()` est entièrement supporté : le programme se met en attente, le terminal affiche le prompt et accepte la saisie clavier.
266
+ ````python
267
+ nom = input("Ton prénom : ")
268
+ print(f"Bonjour, {nom} !")
269
+ ````
270
+
271
+ Ctrl+C interrompt une saisie en cours et retourne `None` — prévoir un guard si nécessaire :
272
+ ````python
273
+ val = input("Valeur : ")
274
+ if val is None:
275
+ print("annulé")
276
+ ````
277
+
278
+ ### rich
279
+
280
+ `rich` est disponible directement — tableaux, couleurs ANSI, barres de progression :
281
+ ````python
282
+ from rich.console import Console
283
+ from rich.table import Table
284
+
285
+ console = Console()
286
+
287
+ table = Table(title="Planètes")
288
+ table.add_column("Nom")
289
+ table.add_column("Diamètre (km)", justify="right")
290
+ table.add_row("Mercure", "4 879")
291
+ table.add_row("Vénus", "12 104")
292
+ table.add_row("Terre", "12 742")
293
+ console.print(table)
294
+ ````
295
+
296
+ > **Boucle d'événements** : le mode terminal tourne dans la boucle asyncio de Pyodide. Pour obtenir un rendu progressif avec `rich.Progress` ou toute autre animation, remplacer `time.sleep()` par `await asyncio.sleep()` et passer `auto_refresh=False` aux composants rich qui gèrent leur propre thread de rafraîchissement :
297
+ >
298
+ > ```python
299
+ > import asyncio
300
+ > from rich.progress import Progress
301
+ >
302
+ > with Progress(auto_refresh=False) as progress:
303
+ > task = progress.add_task("calcul…", total=100)
304
+ > for i in range(100):
305
+ > await asyncio.sleep(0.03)
306
+ > progress.advance(task)
307
+ > progress.refresh()
308
+ > ```
309
+
310
+ ### Relancer
311
+
312
+ Le bouton ▶ (ou `Shift+Entrée`) interrompt proprement le programme en cours (y compris si `input()` est en attente) et relance l'exécution depuis le début.
239
313
 
240
314
  ---
241
315
 
@@ -288,8 +362,7 @@ Tous les blocs Python partagent le même namespace : les variables et fonctions
288
362
  | `type="text/markdown"` | Le contenu est rendu en Markdown (non exécuté) |
289
363
 
290
364
  ### Exemple : énoncé + code utilitaire + zone élève
291
-
292
- ```html
365
+ ````html
293
366
  <script type="text/markdown" data-tab="Énoncé">
294
367
  # Exercice
295
368
 
@@ -313,13 +386,12 @@ def draw():
313
386
  background(20)
314
387
  # à toi de jouer !
315
388
  </script>
316
- ```
389
+ ````
317
390
 
318
391
  ### Bloc caché
319
392
 
320
393
  Un bloc `data-hidden` est concaténé au code exécuté mais invisible dans l'éditeur. Utile pour du code d'initialisation ou de vérification qu'on ne souhaite pas exposer :
321
-
322
- ```html
394
+ ````html
323
395
  <script type="text/python" data-hidden>
324
396
  # Code invisible — exécuté en premier
325
397
  PALETTE = ['#7aa2f7', '#e0af68', '#9ece6a']
@@ -336,7 +408,7 @@ def draw():
336
408
  fill(PALETTE[frameCount // 60 % 3])
337
409
  circle(200, 200, 100)
338
410
  </script>
339
- ```
411
+ ````
340
412
 
341
413
  ### Rendu des onglets Markdown
342
414
 
@@ -356,21 +428,19 @@ Le contenu d'un onglet `type="text/markdown"` est rendu avec [marked](https://ma
356
428
  **Diagrammes (Mermaid) :**
357
429
 
358
430
  Un bloc de code avec le langage `mermaid` est rendu comme un diagramme SVG :
359
-
360
431
  ````markdown
361
432
  ```mermaid
362
- graph TD
363
- A[Départ] --> B{Condition}
364
- B -->|oui| C[Résultat 1]
365
- B -->|non| D[Résultat 2]
433
+ graph TD
434
+ A[Départ] --> B{Condition}
435
+ B -->|oui| C[Résultat 1]
436
+ B -->|non| D[Résultat 2]
366
437
  ```
367
438
  ````
368
439
 
369
440
  Mermaid supporte de nombreux types de diagrammes : `graph`, `sequenceDiagram`, `classDiagram`, `flowchart`, `gantt`, `pie`… Voir la [documentation Mermaid](https://mermaid.js.org/intro/) pour la syntaxe complète.
370
441
 
371
442
  **Exemple combiné :**
372
-
373
- ```html
443
+ ````html
374
444
  <script type="text/markdown" data-tab="Cours">
375
445
  # Algorithme de Dijkstra
376
446
 
@@ -383,18 +453,15 @@ le sommet $u \notin S$ qui minimise :
383
453
  $$d(s, u) = \min_{v \in S} \left( d(s, v) + w(v, u) \right)$$
384
454
 
385
455
  La complexité est $O((V + E) \log V)$ avec un tas binaire.
386
-
387
456
  ```mermaid
388
- graph LR
389
- A((1)) -->|4| B((2))
390
- A -->|1| C((3))
391
- C -->|2| B
392
- B -->|1| D((4))
457
+ graph LR
458
+ A((1)) -->|4| B((2))
459
+ A -->|1| C((3))
460
+ C -->|2| B
461
+ B -->|1| D((4))
393
462
  ```
394
463
  </script>
395
- ```
396
-
397
-
464
+ ````
398
465
 
399
466
  Les blocs Python sont concaténés dans l'ordre du DOM avant exécution : blocs cachés et visibles sont traités ensemble, dans l'ordre où ils apparaissent dans le HTML. L'onglet actif dans l'éditeur n'influence pas l'exécution.
400
467
 
@@ -409,8 +476,7 @@ Par défaut, pyfrilet charge p5.js, Pyodide et ACE depuis des CDN publics. Pour
409
476
  ### Configuration
410
477
 
411
478
  `data-sources` et `data-vendor` se placent de préférence sur la balise `<script src="pyfrilet.js">` elle-même. Pour la rétrocompatibilité, ces attributs sont également acceptés sur le premier bloc `<script type="text/python">`.
412
-
413
- ```html
479
+ ````html
414
480
  <!-- Recommandé -->
415
481
  <script src="pyfrilet.js" data-sources="local" data-vendor="vendor/"></script>
416
482
 
@@ -422,13 +488,12 @@ Par défaut, pyfrilet charge p5.js, Pyodide et ACE depuis des CDN publics. Pour
422
488
  <script type="text/python" data-sources="local" data-vendor="vendor/">
423
489
 
424
490
  </script>
425
- ```
491
+ ````
426
492
 
427
493
  `data-vendor` indique le chemin vers le dossier `vendor/` **relatif à la page HTML**. La valeur par défaut est `vendor/`.
428
494
 
429
495
  ### Structure de fichiers
430
-
431
- ```
496
+ ````
432
497
  mon-projet/
433
498
  ├── pyfrilet.js
434
499
  ├── mon-sketch.html
@@ -439,6 +504,10 @@ mon-projet/
439
504
  ├── theme-monokai.min.js
440
505
  ├── ext-language_tools.min.js
441
506
  ├── ext-searchbox.min.js
507
+ ├── xterm.min.css
508
+ ├── xterm.min.js
509
+ ├── addon-fit.min.js
510
+ ├── addon-unicode11.min.js
442
511
  ├── marked.min.js ← uniquement si onglets Markdown
443
512
  ├── katex.min.css ← uniquement si onglets Markdown
444
513
  ├── katex.min.js ← uniquement si onglets Markdown
@@ -449,51 +518,59 @@ mon-projet/
449
518
  ├── pyodide.asm.wasm
450
519
  ├── python_stdlib.zip
451
520
  └── … (autres fichiers Pyodide)
452
- ```
521
+ ````
453
522
 
454
523
  ### Télécharger les dépendances
455
524
 
456
525
  **p5.js**
457
- ```
526
+ ````
458
527
  https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js
459
- ```
528
+ ````
460
529
 
461
530
  **ACE editor** (5 fichiers)
462
- ```
531
+ ````
463
532
  https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js
464
533
  https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js
465
534
  https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js
466
535
  https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js
467
536
  https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js
468
- ```
537
+ ````
538
+
539
+ **xterm.js** (4 fichiers)
540
+ ````
541
+ https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css
542
+ https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js
543
+ https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js
544
+ https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.8.0/lib/addon-unicode11.min.js
545
+ ````
546
+ Renommer respectivement en `xterm.min.css`, `xterm.min.js`, `addon-fit.min.js`, `addon-unicode11.min.js`.
469
547
 
470
548
  **marked.js + KaTeX + Mermaid** (uniquement si onglets Markdown)
471
- ```
549
+ ````
472
550
  https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js
473
551
  https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css
474
552
  https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js
475
553
  https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js
476
554
  https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js
477
- ```
555
+ ````
478
556
  Renommer `index.umd.js` en `marked-katex-extension.js` dans le dossier vendor.
479
557
 
480
558
  **Pyodide** — télécharger l'archive complète depuis les releases GitHub :
481
- ```
559
+ ````
482
560
  https://github.com/pyodide/pyodide/releases/tag/0.26.4
483
- ```
561
+ ````
484
562
  Extraire le contenu dans `vendor/pyodide/`.
485
563
 
486
564
  ### Serveur local
487
565
 
488
566
  Les navigateurs bloquent le chargement de fichiers locaux (`file://`) pour des raisons de sécurité. Il faut un serveur HTTP minimal pour tester en local :
489
-
490
- ```bash
567
+ ````bash
491
568
  # Python 3
492
569
  python -m http.server 8000
493
570
 
494
571
  # Node.js
495
572
  npx serve .
496
- ```
573
+ ````
497
574
 
498
575
  Puis ouvrir `http://localhost:8000/mon-sketch.html`.
499
576
 
@@ -502,14 +579,12 @@ Puis ouvrir `http://localhost:8000/mon-sketch.html`.
502
579
  ## Build et publication
503
580
 
504
581
  Le build est géré par `build.js` (Node.js + Terser). Il concatène les fragments de `src/`, injecte CSS et HTML, puis génère `pyfrilet.js` (lisible) et `pyfrilet.min.js`.
505
-
506
- ```bash
582
+ ````bash
507
583
  npm run build # génère pyfrilet.js + pyfrilet.min.js
508
- ```
584
+ ````
509
585
 
510
586
  Le hook `prepublishOnly` dans `package.json` déclenche le build automatiquement avant chaque `npm publish`. Le flux complet d'une release :
511
-
512
- ```bash
587
+ ````bash
513
588
  git add .
514
589
  git commit -m "feat: ..." # committer le travail
515
590
 
@@ -517,7 +592,7 @@ npm version patch # (ou minor / major) — modifie package.json, comm
517
592
  npm publish # build automatique puis publication sur npm
518
593
 
519
594
  git push && git push --tags # pousser commits et tag sur Codeberg
520
- ```
595
+ ````
521
596
 
522
597
  ---
523
598
 
@@ -540,10 +615,12 @@ pyfrilet ne contient aucun code de ces bibliothèques ; elles sont chargées sé
540
615
  | [p5.js](https://p5js.org/) | LGPL 2.1 |
541
616
  | [Pyodide](https://pyodide.org/) | MPL 2.0 |
542
617
  | [ACE editor](https://ace.c9.io/) | BSD 3-Clause |
618
+ | [xterm.js](https://xtermjs.org/) | MIT |
543
619
  | [marked](https://marked.js.org/) | MIT |
544
620
  | [KaTeX](https://katex.org/) | MIT |
545
621
  | [marked-katex-extension](https://github.com/UziTech/marked-katex-extension) | MIT |
546
622
  | [Mermaid](https://mermaid.js.org/) | MIT |
623
+ | [rich](https://rich.readthedocs.io/) | MIT |
547
624
 
548
625
  ---
549
626
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyfrilet",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "main": "pyfrilet.js",
6
6
  "files": [
package/pyfrilet.js CHANGED
@@ -39,6 +39,10 @@ const CDN = {
39
39
  katex : 'https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js',
40
40
  markedKatex : 'https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js',
41
41
  mermaid : 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js',
42
+ xtermCss : 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css',
43
+ xterm : 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js',
44
+ xtermFit : 'https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js',
45
+ xtermUni : 'https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.8.0/lib/addon-unicode11.min.js',
42
46
  };
43
47
 
44
48
  /* ═══════════════════════════ STYLES ═════════════════════════════════ */
@@ -243,7 +247,7 @@ const STYLES = `html, body {
243
247
  .pf-tab.pf-tab-markdown::after { content: ' ✎'; font-size: 11px; opacity: .6; }
244
248
 
245
249
  /* ── markdown view ── */
246
- @import url('https://fonts.googleapis.com/css2?family=Alegreya+Sans:ital,wght@0,400;0,700;1,400&display=swap');
250
+ @import url('https://fonts.googleapis.com/css2?family=Alegreya+Sans:ital,wght@0,400;0,700;1,400&family=Fira+Code:wght@300..700&display=swap');
247
251
 
248
252
  #pf-markdown-view {
249
253
  flex: 1;
@@ -380,12 +384,36 @@ const STYLES = `html, body {
380
384
  white-space: pre-wrap;
381
385
  display: none;
382
386
  border-top: 1px solid rgba(247, 118, 142, .35);
383
- }`;
387
+ }
388
+ /* ── xterm terminal ── */
389
+ #pf-xterm {
390
+ display: none;
391
+ position: absolute;
392
+ inset: 0;
393
+ padding: 10px 12px;
394
+ box-sizing: border-box;
395
+ background: #000000;
396
+ overflow: hidden;
397
+ }
398
+
399
+ #pf-xterm.pf-xterm-overlay {
400
+ background: rgba(0, 0, 0, 0.82);
401
+ }
402
+
403
+ /* xterm interne : prendre toute la hauteur */
404
+ #pf-xterm .xterm {
405
+ height: 100%;
406
+ }
407
+ #pf-xterm .xterm-screen {
408
+ height: 100% !important;
409
+ }
410
+ `;
384
411
 
385
412
  /* ═══════════════════════════ MARKUP ═════════════════════════════════ */
386
413
  const MARKUP = `<div id="pf-root">
387
414
  <div id="pf-app" tabindex="-1">
388
415
  <div id="pf-viewport"><div id="pf-sketch"></div></div>
416
+ <div id="pf-xterm"></div>
389
417
  <div id="pf-loader">
390
418
  <span id="pf-loader-msg">Chargement…</span>
391
419
  <div id="pf-loader-bar"></div>
@@ -461,6 +489,10 @@ document.addEventListener('DOMContentLoaded', function () {
461
489
  katex : hasMarked ? CDN.katex : null,
462
490
  markedKatex : hasMarked ? CDN.markedKatex : null,
463
491
  mermaid : hasMarked ? CDN.mermaid : null,
492
+ xtermCss : CDN.xtermCss,
493
+ xterm : CDN.xterm,
494
+ xtermFit : CDN.xtermFit,
495
+ xtermUni : CDN.xtermUni,
464
496
  } : {
465
497
  p5 : vp + 'p5.min.js',
466
498
  pyodide : vp + 'pyodide/pyodide.js',
@@ -475,6 +507,10 @@ document.addEventListener('DOMContentLoaded', function () {
475
507
  katex : hasMarked ? vp + 'katex.min.js' : null,
476
508
  markedKatex : hasMarked ? vp + 'marked-katex-extension.js' : null,
477
509
  mermaid : hasMarked ? vp + 'mermaid.min.js' : null,
510
+ xtermCss : vp + 'xterm.min.css',
511
+ xterm : vp + 'xterm.min.js',
512
+ xtermFit : vp + 'xterm-addon-fit.min.js',
513
+ xtermUni : vp + 'addon-unicode11.min.js',
478
514
  };
479
515
 
480
516
  const SK = 'pyfrilet:' + location.pathname;
@@ -1097,6 +1133,7 @@ function main(tabs, htmlTabs, SK, URLS, noWatchdog, staleSnapshot) {
1097
1133
  const opts = {};
1098
1134
  if (URLS.pyodideIndex) opts.indexURL = URLS.pyodideIndex;
1099
1135
  pyodide = await loadPyodide(opts);
1136
+ await pyodide.loadPackage(['rich', 'pygments']);
1100
1137
 
1101
1138
  /* Build the "p5" Python module via dynamic introspection of a dummy instance */
1102
1139
  pyodide.runPython(`
@@ -1272,17 +1309,47 @@ def _wdog_trace(frame, event, arg):
1272
1309
  raise TimeoutError("draw() watchdog")
1273
1310
  return _wdog_trace
1274
1311
 
1312
+ from rich.console import Console as _RichConsole
1313
+ _pf_rich_console = _RichConsole(stderr=True)
1314
+
1315
+ class _PfHandledError(Exception):
1316
+ """Levée après que rich a déjà affiché le traceback vers xterm."""
1317
+ pass
1318
+
1319
+ def _pf_safe_call(fn):
1320
+ try:
1321
+ fn()
1322
+ except (_PfHandledError, TimeoutError):
1323
+ raise
1324
+ except Exception as _e:
1325
+ _tb = _e.__traceback__
1326
+ while _tb and _tb.tb_frame.f_code.co_filename not in ('<string>', '<pyfrilet>'):
1327
+ _tb = _tb.tb_next
1328
+ if _tb: _e.__traceback__ = _tb
1329
+ _pf_rich_console.print_exception(show_locals=False)
1330
+ from js import _pfShowErrorTerminal
1331
+ _pfShowErrorTerminal()
1332
+
1333
+ def _pf_safe_proxy(fn):
1334
+ from pyodide.ffi import create_proxy as _cp
1335
+ def _wrapped(*args, **kwargs):
1336
+ _pf_safe_call(lambda: fn(*args, **kwargs))
1337
+ return _cp(_wrapped)
1338
+
1339
+ setattr(m, 'safe_proxy', _pf_safe_proxy)
1340
+ _p5_functions.add('safe_proxy')
1341
+
1275
1342
  def _pf_draw_watchdog(fn, timeout_ms):
1276
1343
  _wdog_count[0] = 0
1277
1344
  _wdog_deadline[0] = _time.monotonic() + timeout_ms * 0.001
1278
1345
  sys.settrace(_wdog_trace)
1279
1346
  try:
1280
- fn()
1347
+ _pf_safe_call(fn)
1281
1348
  finally:
1282
1349
  sys.settrace(None)
1283
1350
 
1284
1351
  def _pf_draw_direct(fn, timeout_ms):
1285
- fn()
1352
+ _pf_safe_call(fn)
1286
1353
 
1287
1354
  def _snake_to_camel(name):
1288
1355
  parts = name.split('_')
@@ -1310,6 +1377,71 @@ def _p5_getattr(name):
1310
1377
  m.__getattr__ = _p5_getattr
1311
1378
  `);
1312
1379
 
1380
+
1381
+ /* ── Terminal mode: async execution with input() support ── */
1382
+ pyodide.runPython(`
1383
+ import asyncio as _asyncio, ast as _ast
1384
+ import os as _os, sys as _sys
1385
+ _os.environ.setdefault('TERM', 'xterm-256color')
1386
+ _os.environ.setdefault('COLORTERM', 'truecolor')
1387
+
1388
+ # Wrapper file-like qui écrit directement vers xterm via JS.
1389
+ # Rich écrit des strings sur sys.stdout.write() — il faut un vrai objet fichier.
1390
+ class _PfStream:
1391
+ def __init__(self, js_fn):
1392
+ self._fn = js_fn
1393
+ self.encoding = 'utf-8'
1394
+ self.errors = 'replace'
1395
+ def write(self, s):
1396
+ if s:
1397
+ self._fn(s)
1398
+ return len(s)
1399
+ def writelines(self, lines):
1400
+ for l in lines: self.write(l)
1401
+ def flush(self): pass
1402
+ def isatty(self): return True
1403
+ @property
1404
+ def softspace(self): return 0
1405
+
1406
+ from js import _pfTermWrite, _pfTermWriteErr
1407
+ _sys.stdout = _PfStream(_pfTermWrite)
1408
+ _sys.stderr = _PfStream(_pfTermWriteErr)
1409
+
1410
+ async def _pf_async_input(prompt=""):
1411
+ from js import _pfTerminalInput
1412
+ result = await _pfTerminalInput(str(prompt) if prompt else "")
1413
+ return result
1414
+
1415
+ async def _pf_run_terminal(source):
1416
+ class _InputAwaiter(_ast.NodeTransformer):
1417
+ def visit_Call(self, node):
1418
+ self.generic_visit(node)
1419
+ if isinstance(node.func, _ast.Name) and node.func.id == 'input':
1420
+ return _ast.Await(value=node)
1421
+ return node
1422
+
1423
+ tree = _ast.parse(source)
1424
+ tree = _InputAwaiter().visit(tree)
1425
+
1426
+ wrapper = _ast.parse("async def programme(): pass")
1427
+ wrapper.body[0].body = tree.body if tree.body else [_ast.Pass()]
1428
+ _ast.fix_missing_locations(wrapper)
1429
+
1430
+ _ns = {'input': _pf_async_input}
1431
+ exec(compile(wrapper, '<pyfrilet>', 'exec'), _ns)
1432
+ try:
1433
+ await _ns['programme']()
1434
+ except SystemExit:
1435
+ pass
1436
+ except Exception as _e:
1437
+ _tb = _e.__traceback__
1438
+ while _tb and _tb.tb_frame.f_code.co_filename != '<pyfrilet>':
1439
+ _tb = _tb.tb_next
1440
+ if _tb:
1441
+ _e.__traceback__ = _tb
1442
+ _pf_rich_console.print_exception(show_locals=False)
1443
+ `);
1444
+
1313
1445
  /* Inject p5 symbols into ACE autocomplete */
1314
1446
  if (aceInst) {
1315
1447
  const p5all = pyodide.runPython('list(m.__all__)').toJs();
@@ -1348,6 +1480,7 @@ m.__getattr__ = _p5_getattr
1348
1480
 
1349
1481
  /* ─────────────────── RUN CODE ───────────────── */
1350
1482
  let running = false;
1483
+ let _terminalRunning = false; /* true pendant qu'un runTerminalCode est actif */
1351
1484
  let setupProxy = null, drawProxy = null,
1352
1485
  preloadProxy = null,
1353
1486
  mousePressedProxy = null, mouseReleasedProxy = null, mouseDraggedProxy = null,
@@ -1357,11 +1490,47 @@ m.__getattr__ = _p5_getattr
1357
1490
 
1358
1491
  const WATCHDOG_MS = 300;
1359
1492
 
1493
+ /* ─────────────────── TERMINAL MODE ────────────── */
1494
+ function isTerminalMode(code) {
1495
+ /* Terminal mode = no p5 import detected */
1496
+ return !/\bfrom\s+p5\s+import\b|\bimport\s+p5\b/.test(code);
1497
+ }
1498
+
1499
+ async function runTerminalCode(code) {
1500
+ _terminalRunning = true;
1501
+ showTerminal();
1502
+ termClear();
1503
+ clearError();
1504
+
1505
+ try {
1506
+ const runner = pyodide.globals.get('_pf_run_terminal');
1507
+ await runner(code);
1508
+ } catch (e) {
1509
+ /* Seules les erreurs avant l'exec arrivent ici (ex: SyntaxError de parsing) */
1510
+ const msg = String(e);
1511
+ if (!msg.includes('SystemExit')) termWriteErr(msg + '\n');
1512
+ } finally {
1513
+ _terminalRunning = false;
1514
+ }
1515
+ }
1516
+
1360
1517
  async function runCode() {
1361
- if (running) return;
1518
+ if (running) {
1519
+ if (_terminalRunning) {
1520
+ /* Terminal en cours (ex: input() en attente) — annuler et relancer */
1521
+ hideTerminal(); /* résout le _pfTerminalInput en attente avec '' */
1522
+ running = false;
1523
+ btnRun.classList.remove('pf-running');
1524
+ /* Laisser la micro-tâche Python se terminer avant de relancer */
1525
+ await new Promise(r => setTimeout(r, 30));
1526
+ } else {
1527
+ return; /* sketch p5 en cours — ignorer */
1528
+ }
1529
+ }
1362
1530
  running = true;
1363
1531
  btnRun.classList.add('pf-running');
1364
1532
  clearError();
1533
+ termClear();
1365
1534
  stopSketch();
1366
1535
 
1367
1536
  if (!pyodide) {
@@ -1373,7 +1542,7 @@ m.__getattr__ = _p5_getattr
1373
1542
  await ensurePyodide();
1374
1543
  } catch (e) {
1375
1544
  loaderEl.style.display = 'none';
1376
- showError('Erreur Pyodide : ' + e);
1545
+ showError('Erreur Pyodide : ' + (e.message || String(e)));
1377
1546
  running = false; btnRun.classList.remove('pf-running'); return;
1378
1547
  }
1379
1548
 
@@ -1396,13 +1565,22 @@ m.__getattr__ = _p5_getattr
1396
1565
  } catch (e) { console.warn('[pyfrilet] loadPackagesFromImports:', e); }
1397
1566
  loaderEl.style.display = 'none';
1398
1567
 
1568
+
1569
+ /* ── Route: terminal mode (no p5) or p5 mode ── */
1570
+ if (isTerminalMode(code)) {
1571
+ btnRun.classList.remove('pf-running'); /* bouton actif pendant l'exécution terminal */
1572
+ await runTerminalCode(code);
1573
+ running = false; return;
1574
+ }
1575
+ hideTerminal(); /* make sure terminal hidden for p5 mode */
1576
+
1399
1577
  pyodide.globals.set('_USER_CODE', code);
1400
1578
 
1401
1579
  try {
1402
1580
  pyodide.runPython('_ns = {}; exec(_USER_CODE, _ns, _ns)');
1403
1581
  pyodide.runPython('_ns_ref[0] = _ns'); /* give size() access to current ns */
1404
1582
  } catch (e) {
1405
- showError(String(e));
1583
+ showErrorTerminal(e.message || String(e));
1406
1584
  running = false; btnRun.classList.remove('pf-running'); return;
1407
1585
  }
1408
1586
 
@@ -1425,7 +1603,7 @@ m.__getattr__ = _p5_getattr
1425
1603
  pyTM = _get('touchMoved', 'touch_moved');
1426
1604
  pyTE = _get('touchEnded', 'touch_ended');
1427
1605
  } catch (e) {
1428
- showError(String(e));
1606
+ showErrorTerminal(e.message || String(e));
1429
1607
  running = false; btnRun.classList.remove('pf-running'); return;
1430
1608
  }
1431
1609
 
@@ -1439,13 +1617,15 @@ m.__getattr__ = _p5_getattr
1439
1617
  const pyRefresh = pyodide.globals.get('_pf_refresh');
1440
1618
  const pfDrawWatchdog = pyodide.globals.get(noWatchdog ? '_pf_draw_direct' : '_pf_draw_watchdog');
1441
1619
  const pyNs = pyodide.globals.get('_ns');
1620
+ const pfSafeCall = pyodide.globals.get('_pf_safe_call');
1442
1621
 
1443
1622
  const mkProxy = (fn) => fn ? create_proxy(() => {
1444
- try { pyRefresh(pyNs); fn(); } catch (e) { showError(String(e)); }
1623
+ try { pyRefresh(pyNs); pfSafeCall(fn); } catch (e) { showErrorTerminal(''); }
1445
1624
  }) : null;
1446
1625
 
1447
- preloadProxy = pyPreload ? create_proxy(() => { try { pyPreload(); } catch (e) { showError(String(e)); } }) : null;
1448
- setupProxy = pySetup ? create_proxy(() => { try { pySetup(); } catch (e) { showError(String(e)); } }) : null;
1626
+ preloadProxy = pyPreload ? create_proxy(() => { try { pfSafeCall(pyPreload); } catch (e) { showErrorTerminal(''); } }) : null;
1627
+ setupProxy = pySetup ? create_proxy(() => { try { pfSafeCall(pySetup); } catch (e) { showErrorTerminal(''); } }) : null;
1628
+
1449
1629
  drawProxy = create_proxy(() => {
1450
1630
  try {
1451
1631
  pyRefresh(pyNs);
@@ -1456,7 +1636,7 @@ m.__getattr__ = _p5_getattr
1456
1636
  if (msg.includes('TimeoutError') || msg.includes('watchdog')) {
1457
1637
  showError(`draw() a dépassé ${WATCHDOG_MS}ms — sketch arrêté (watchdog).`);
1458
1638
  } else {
1459
- showError(msg);
1639
+ showErrorTerminal('');
1460
1640
  }
1461
1641
  }
1462
1642
  });
@@ -1471,7 +1651,7 @@ m.__getattr__ = _p5_getattr
1471
1651
  touchStartedProxy = mkProxy(pyTS);
1472
1652
  touchMovedProxy = mkProxy(pyTM);
1473
1653
  touchEndedProxy = mkProxy(pyTE);
1474
- const windowResizedProxy = pyWR ? create_proxy(() => { try { pyWR(); } catch (e) { showError(String(e)); } }) : null;
1654
+ const windowResizedProxy = pyWR ? create_proxy(() => { try { pfSafeCall(pyWR); } catch (e) { showErrorTerminal(''); } }) : null;
1475
1655
 
1476
1656
  let setupDone = false;
1477
1657
  pInst = new p5((p) => {
@@ -1517,7 +1697,7 @@ m.__getattr__ = _p5_getattr
1517
1697
  }
1518
1698
 
1519
1699
  /* ─────────────────── DOWNLOAD ───────────────── */
1520
- const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.5.3/pyfrilet.min.js';
1700
+ const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.6.0/pyfrilet.min.js';
1521
1701
 
1522
1702
  const STANDALONE_TEMPLATE = `<!doctype html>
1523
1703
  <html lang="fr">
@@ -1734,6 +1914,14 @@ FILLME-SCRIPTS
1734
1914
  await loadScript(URLS.aceLangTools);
1735
1915
  await loadScript(URLS.aceSearchbox);
1736
1916
  await loadScript(URLS.pyodide);
1917
+ /* xterm — terminal pour mode script sans p5 */
1918
+ const xtermLink = document.createElement('link');
1919
+ xtermLink.rel = 'stylesheet';
1920
+ xtermLink.href = URLS.xtermCss;
1921
+ document.head.appendChild(xtermLink);
1922
+ await loadScript(URLS.xterm);
1923
+ await loadScript(URLS.xtermFit);
1924
+ await loadScript(URLS.xtermUni);
1737
1925
  } catch (e) {
1738
1926
  loaderMsg.textContent = '⚠ ' + e.message;
1739
1927
  document.getElementById('pf-loader-bar').style.display = 'none';
@@ -1745,6 +1933,163 @@ FILLME-SCRIPTS
1745
1933
  loaderEl.style.display = 'none';
1746
1934
  })();
1747
1935
 
1936
+ /* ─────────────────── TERMINAL (xterm.js) ──────── */
1937
+ const xtermEl = document.getElementById('pf-xterm');
1938
+
1939
+ let _xterm = null; /* Terminal instance */
1940
+ let _fitAddon = null; /* FitAddon instance */
1941
+
1942
+ /* Input state */
1943
+ let _inputResolve = null; /* non-null ↔ en attente d'une ligne */
1944
+ let _lineBuffer = ''; /* caractères tapés sur la ligne courante */
1945
+
1946
+ /* ── Initialise xterm (appelé une seule fois) ── */
1947
+ function _initXterm() {
1948
+ if (_xterm) return;
1949
+
1950
+ _xterm = new Terminal({
1951
+ theme: {
1952
+ background : '#000000',
1953
+ foreground : '#e8e8e8',
1954
+ cursor : '#ffffff',
1955
+ black : '#2a2a2a', brightBlack : '#555555',
1956
+ red : '#cc4444', brightRed : '#ff6666',
1957
+ green : '#44aa44', brightGreen : '#66cc66',
1958
+ yellow : '#aaaa00', brightYellow : '#dddd44',
1959
+ blue : '#4466cc', brightBlue : '#6688ff',
1960
+ magenta : '#aa44aa', brightMagenta: '#dd66dd',
1961
+ cyan : '#44aaaa', brightCyan : '#66cccc',
1962
+ white : '#cccccc', brightWhite : '#ffffff',
1963
+ },
1964
+ fontFamily : "'Fira Code', 'Consolas', 'Courier New', monospace",
1965
+ fontSize : 15,
1966
+ lineHeight : 1.0,
1967
+ letterSpacing : 0,
1968
+ cursorBlink : true,
1969
+ scrollback : 2000,
1970
+ convertEol : true,
1971
+ allowProposedApi: true,
1972
+ });
1973
+
1974
+ _fitAddon = new FitAddon.FitAddon();
1975
+ _xterm.loadAddon(_fitAddon);
1976
+
1977
+ /* addon-unicode11 : emoji et CJK comptés comme 2 cellules (= wcwidth) */
1978
+ const _uniAddon = new Unicode11Addon.Unicode11Addon();
1979
+ _xterm.loadAddon(_uniAddon);
1980
+ _xterm.unicode.activeVersion = '11';
1981
+
1982
+ _xterm.open(xtermEl);
1983
+ _fitAddon.fit();
1984
+
1985
+ /* Redimensionner avec le drawer */
1986
+ new ResizeObserver(() => {
1987
+ if (_xterm && xtermEl.style.display !== 'none') _fitAddon.fit();
1988
+ }).observe(xtermEl);
1989
+
1990
+ /* ── Gestion clavier : édition de ligne + soumission ── */
1991
+ _xterm.onData(e => {
1992
+ if (!_inputResolve) return;
1993
+
1994
+ if (e === '\r') { /* Entrée */
1995
+ const val = _lineBuffer;
1996
+ _lineBuffer = '';
1997
+ _xterm.write('\r\n');
1998
+ const res = _inputResolve;
1999
+ _inputResolve = null;
2000
+ res(val);
2001
+
2002
+ } else if (e === '\x7f') { /* Backspace */
2003
+ if (_lineBuffer.length > 0) {
2004
+ _lineBuffer = _lineBuffer.slice(0, -1);
2005
+ _xterm.write('\b \b');
2006
+ }
2007
+
2008
+ } else if (e === '\x03') { /* Ctrl+C */
2009
+ _lineBuffer = '';
2010
+ _xterm.write('^C\r\n');
2011
+ const res = _inputResolve;
2012
+ _inputResolve = null;
2013
+ res(null);
2014
+
2015
+ } else if (e.charCodeAt(0) >= 32) { /* imprimable */
2016
+ _lineBuffer += e;
2017
+ _xterm.write(e);
2018
+ }
2019
+ });
2020
+ }
2021
+
2022
+ /* ── Exposé à Python comme js._pfTerminalInput(prompt) ── */
2023
+ window._pfTerminalInput = function (prompt) {
2024
+ return new Promise(resolve => {
2025
+ _inputResolve = resolve;
2026
+ _lineBuffer = '';
2027
+ if (prompt) _xterm.write(prompt);
2028
+ _xterm.focus();
2029
+ });
2030
+ };
2031
+
2032
+ /* ── stdout / stderr — exposés comme globals pour le wrapper Python ── */
2033
+ function termWrite(text) {
2034
+ if (_xterm) _xterm.write(text);
2035
+ }
2036
+ window._pfTermWrite = termWrite;
2037
+
2038
+ function termWriteErr(text) {
2039
+ _initXterm();
2040
+ _xterm.write('\x1b[31m');
2041
+ _xterm.write(text.replace(/\n/g, '\r\n'));
2042
+ _xterm.write('\x1b[0m');
2043
+ }
2044
+ window._pfTermWriteErr = termWriteErr;
2045
+
2046
+ /* ── Clear ── */
2047
+ function termClear() {
2048
+ if (_xterm) { _xterm.clear(); }
2049
+ _inputResolve = null;
2050
+ _lineBuffer = '';
2051
+ }
2052
+
2053
+ /* ── Afficher / cacher ── */
2054
+ function showTerminal() {
2055
+ viewEl.style.display = 'none';
2056
+ xtermEl.style.display = 'block';
2057
+ xtermEl.classList.remove('pf-xterm-overlay');
2058
+ _initXterm();
2059
+ _fitAddon.fit();
2060
+ _xterm.focus();
2061
+ }
2062
+
2063
+ window._pfShowErrorTerminal = () => {
2064
+ stopSketch();
2065
+ showErrorTerminal('');
2066
+ };
2067
+
2068
+ /* Overlay semi-transparent par-dessus le sketch — pour les erreurs p5 */
2069
+ function showErrorTerminal(msg) {
2070
+ xtermEl.style.display = 'block';
2071
+ xtermEl.classList.add('pf-xterm-overlay');
2072
+ _initXterm();
2073
+ _fitAddon.fit();
2074
+ if (msg) {
2075
+ termClear();
2076
+ _xterm.write('\x1b[1;31m── Erreur ──────────────────────────────────────\x1b[0m\r\n\r\n');
2077
+ _xterm.write(msg.replace(/\n/g, '\r\n') + '\r\n');
2078
+ }
2079
+ }
2080
+
2081
+ function hideTerminal() {
2082
+ xtermEl.style.display = 'none';
2083
+ xtermEl.classList.remove('pf-xterm-overlay');
2084
+ viewEl.style.display = '';
2085
+ if (_inputResolve) {
2086
+ const res = _inputResolve;
2087
+ _inputResolve = null;
2088
+ _lineBuffer = '';
2089
+ res('');
2090
+ }
2091
+ }
2092
+
1748
2093
 
1749
2094
  } /* end main() */
1750
2095
 
package/pyfrilet.min.js CHANGED
@@ -1 +1 @@
1
- !function(){"use strict";const e=document.currentScript;let n=!1;const t="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",a="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js",o="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js",r="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",i="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js",s="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js",d="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",l="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js",c="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css",p="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js",m="https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js",f="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js",u="html, body {\n height: 100%; margin: 0; overflow: hidden;\n background: #111;\n}\n#pf-root {\n position: fixed; inset: 0;\n display: flex; flex-direction: column;\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n}\n\n/* ── app area ── */\n#pf-app:focus { outline: none; }\n#pf-app {\n flex: 1; min-height: 0;\n position: relative;\n background: #111;\n display: flex; align-items: center; justify-content: center;\n overflow: hidden;\n}\n#pf-viewport {\n transform-origin: 50% 50%;\n will-change: transform;\n}\n#pf-viewport canvas {\n display: block;\n outline: none;\n}\n#pf-loader {\n position: absolute; inset: 0;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n gap: 14px;\n background: #111;\n color: #565f89;\n font-size: 13px;\n z-index: 50;\n pointer-events: none;\n}\n#pf-loader-bar {\n width: 160px; height: 2px;\n background: #2a2c3e;\n border-radius: 2px;\n overflow: hidden;\n}\n#pf-loader-bar::after {\n content: '';\n display: block;\n height: 100%;\n width: 40%;\n background: #7aa2f7;\n border-radius: 2px;\n animation: pf-slide 1.2s ease-in-out infinite;\n}\n@keyframes pf-slide {\n 0% { transform: translateX(-100%); }\n 100% { transform: translateX(350%); }\n}\n\n/* ── drawer (slide-up editor panel) ── */\n#pf-drawer {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n background: #1a1b26;\n height: 32px; /* collapsed = handle only */\n transition: height 0.26s cubic-bezier(.4, 0, .2, 1);\n overflow: hidden;\n /* shadow cast upward onto the app */\n box-shadow: 0 -4px 20px rgba(0,0,0,.55);\n}\n#pf-drawer.pf-open {\n height: var(--pf-drawer-h, 56vh);\n}\n\n/* ── handle bar ── */\n#pf-handle {\n height: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n padding: 0 8px 0 6px;\n background: #24283b;\n border-top: 1px solid #3d4166;\n cursor: ns-resize;\n user-select: none;\n gap: 6px;\n flex-shrink: 0;\n}\n/* grip zone: clickable to toggle, draggable to resize */\n#pf-grip {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 5px 6px;\n flex-shrink: 0;\n opacity: .5;\n border-radius: 4px;\n transition: opacity .15s, background .15s;\n cursor: pointer;\n}\n#pf-grip:hover { opacity: .85; background: rgba(255,255,255,.06); }\n#pf-grip span {\n display: block;\n width: 16px; height: 2px;\n background: #a9b1d6;\n border-radius: 1px;\n}\n#pf-handle-hint {\n flex: 1;\n color: #565f89;\n font-size: 10px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n#pf-handle-btns {\n display: flex;\n gap: 4px;\n flex-shrink: 0;\n}\n.pf-btn {\n height: 26px;\n min-width: 26px;\n padding: 0 5px;\n border: 0; border-radius: 5px;\n cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n font-size: 13px; line-height: 1;\n white-space: nowrap;\n transition: background .15s, transform .1s, opacity .15s;\n outline: none;\n box-sizing: border-box;\n}\n.pf-btn:active { transform: scale(.88); }\n.pf-btn:focus-visible { outline: 2px solid #7aa2f7; outline-offset: 1px; }\n\n#pf-btn-run { background: #1a6b3a; color: #9ece6a; font-size: 11px; }\n#pf-btn-run:hover { background: #1f8447; color: #b9f27a; }\n#pf-btn-run.pf-running { opacity: .5; cursor: not-allowed; }\n\n#pf-btn-code { background: #2a2c3e; color: #7aa2f7; font-size: 14px; }\n#pf-btn-code:hover { background: #3d4166; color: #c0caf5; }\n#pf-btn-code.pf-active { background: #3d4166; color: #e0af68; }\n\n#pf-btn-dl { background: #2a2c3e; color: #9d7cd8; font-size: 14px; }\n#pf-btn-dl:hover { background: #3d4166; color: #bb9af7; }\n\n#pf-btn-rec { background: #2a2c3e; color: #f7768e; font-size: 13px; }\n#pf-btn-rec:hover { background: #3d4166; color: #ff9e9e; }\n#pf-btn-rec.pf-recording { background: #6b1a1a; color: #f7768e; animation: pf-blink .8s step-end infinite; }\n@keyframes pf-blink { 50% { opacity: .4; } }\n\n#pf-btn-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }\n#pf-btn-reset:hover { background: #3d4166; color: #ffc777; }\n#pf-btn-reset.pf-dirty::after {\n content: '●';\n position: absolute;\n top: 2px; right: 3px;\n font-size: 7px;\n color: #e0af68;\n line-height: 1;\n}\n#pf-btn-reset { position: relative; }\n\n/* ── editor area inside drawer ── */\n#pf-editor-wrap {\n flex: 1;\n min-height: 80px;\n position: relative;\n display: flex;\n flex-direction: column;\n}\n#pf-ace { flex: 1; position: relative; min-height: 0; }\n\n/* ── tab bar ── */\n#pf-tabs {\n display: flex;\n flex-shrink: 0;\n background: #1a1b2e;\n border-bottom: 1px solid #414868;\n overflow-x: auto;\n scrollbar-width: none;\n}\n#pf-tabs:empty { display: none; }\n.pf-tab {\n padding: 5px 14px;\n font-size: 12px;\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n color: #737aa2;\n cursor: pointer;\n white-space: nowrap;\n transition: color .15s, border-color .15s;\n}\n.pf-tab:hover { color: #c0caf5; }\n.pf-tab.pf-tab-active { color: #c0caf5; border-bottom-color: #7aa2f7; }\n.pf-tab.pf-tab-readonly::after { content: ' 🔒'; font-size: 10px; opacity: .6; }\n.pf-tab.pf-tab-markdown::after { content: ' ✎'; font-size: 11px; opacity: .6; }\n\n/* ── markdown view ── */\n@import url('https://fonts.googleapis.com/css2?family=Alegreya+Sans:ital,wght@0,400;0,700;1,400&display=swap');\n\n#pf-markdown-view {\n flex: 1;\n overflow: auto;\n background: #f4f4f0;\n}\n\n#pf-markdown-view .pf-md-inner {\n width: 100%;\n max-width: 680px;\n margin: 0 auto;\n padding: 48px 48px 72px;\n box-sizing: border-box;\n font-family: 'Alegreya Sans', Georgia, serif;\n font-size: 17px;\n line-height: 1.8;\n color: #1c1c2e;\n}\n\n#pf-markdown-view h1 {\n font-size: 2.1em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 0 0 .3em;\n padding-bottom: .3em;\n border-bottom: 2px solid #d8d8e8;\n line-height: 1.2;\n}\n#pf-markdown-view h2 {\n font-size: 1.4em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 2em 0 .5em;\n padding-bottom: .2em;\n border-bottom: 1px solid #e0e0ec;\n}\n#pf-markdown-view h3 {\n font-size: 1.1em;\n font-weight: 700;\n color: #2a2a4a;\n margin: 1.6em 0 .4em;\n}\n\n#pf-markdown-view p { margin: .75em 0; }\n#pf-markdown-view ul,\n#pf-markdown-view ol { padding-left: 1.6em; margin: .75em 0; }\n#pf-markdown-view li { margin: .3em 0; }\n#pf-markdown-view hr { border: none; border-top: 1px solid #dde; margin: 2em 0; }\n#pf-markdown-view blockquote {\n margin: 1em 0;\n padding: .5em 1em;\n border-left: 3px solid #aab;\n color: #555;\n background: #ededf5;\n border-radius: 0 4px 4px 0;\n}\n\n#pf-markdown-view code {\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n font-size: .84em;\n background: #e8e8f2;\n color: #3a3a6a;\n padding: .15em .45em;\n border-radius: 4px;\n}\n#pf-markdown-view pre {\n background: #1a1b2e;\n border-radius: 8px;\n padding: 1em 1.2em;\n overflow: auto;\n margin: 1.2em 0;\n box-shadow: 0 2px 8px rgba(0,0,0,.12);\n}\n#pf-markdown-view pre code {\n background: transparent;\n color: #c0caf5;\n font-size: .86em;\n padding: 0;\n line-height: 1.6;\n border-radius: 0;\n}\n\n#pf-markdown-view table {\n border-collapse: collapse;\n width: 100%;\n margin: 1.2em 0;\n font-size: .95em;\n}\n#pf-markdown-view th {\n background: #e4e4f0;\n color: #1c1c2e;\n font-weight: 700;\n text-align: left;\n padding: .55em .85em;\n border: 1px solid #d0d0e8;\n}\n#pf-markdown-view td {\n padding: .5em .85em;\n border: 1px solid #e0e0ee;\n vertical-align: top;\n}\n#pf-markdown-view tr:nth-child(even) td { background: #f0f0f8; }\n\n#pf-markdown-view a {\n color: #3a5fc8;\n text-decoration: none;\n border-bottom: 1px solid rgba(58,95,200,.3);\n transition: color .15s, border-color .15s;\n}\n#pf-markdown-view a:hover { color: #1a3fa0; border-bottom-color: #1a3fa0; }\n\n#pf-markdown-view .katex-display {\n overflow-x: auto;\n padding: .5em 0;\n margin: 1.2em 0;\n}\n#pf-markdown-view .mermaid {\n text-align: center;\n margin: 1.5em 0;\n background: #ededf5;\n border-radius: 8px;\n padding: 1em;\n}\n\n/* ── error panel (below editor, never overlaps ACE) ── */\n#pf-err {\n flex-shrink: 0;\n max-height: 120px;\n overflow: auto;\n margin: 0; padding: 8px 13px;\n font-size: 11.5px; line-height: 1.45;\n background: rgba(13, 3, 3, .95);\n color: #f7768e;\n white-space: pre-wrap;\n display: none;\n border-top: 1px solid rgba(247, 118, 142, .35);\n}",h='<div id="pf-root">\n <div id="pf-app" tabindex="-1">\n <div id="pf-viewport"><div id="pf-sketch"></div></div>\n <div id="pf-loader">\n <span id="pf-loader-msg">Chargement…</span>\n <div id="pf-loader-bar"></div>\n </div>\n </div>\n <div id="pf-drawer">\n <div id="pf-handle">\n <div id="pf-grip" title="Clic → ouvrir/fermer"><span></span><span></span><span></span></div>\n <span id="pf-handle-hint">Clic ☰ → ouvrir/fermer &nbsp;·&nbsp; Shift+Entrée → relancer</span>\n <div id="pf-handle-btns">\n <button class="pf-btn" id="pf-btn-run" title="Relancer (Shift+Entrée)">&#9654;</button>\n <button class="pf-btn" id="pf-btn-code" title="Éditeur plein écran">&#9999;&#xFE0F;</button>\n <button class="pf-btn" id="pf-btn-dl" title="Télécharger HTML autonome">&#128190;</button>\n <button class="pf-btn" id="pf-btn-rec" title="Enregistrer WebM">⏺</button>\n <button class="pf-btn" id="pf-btn-help" title="Aide">?</button>\n <button class="pf-btn" id="pf-btn-reset" title="Réinitialiser le code (Ctrl+R)">&#8635;</button>\n </div>\n </div>\n <div id="pf-editor-wrap">\n <div id="pf-tabs"></div>\n <div id="pf-markdown-view" style="display:none"></div>\n <div id="pf-ace"></div>\n </div>\n <pre id="pf-err"></pre>\n </div>\n</div>';document.addEventListener("DOMContentLoaded",function(){const _=[...document.querySelectorAll('script[type="text/python"], script[type="text/markdown"], python')];if(0===_.length)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const y=e||_[0],g=(y.getAttribute("data-sources")||y.getAttribute("sources")||"cdn").toLowerCase().trim(),b=(y.getAttribute("data-vendor")||y.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");n="cdn"===g;const v=_.some(e=>"text/markdown"===e.getAttribute("type")),w=n?{p5:t,pyodide:a,pyodideIndex:null,ace:o,acePython:r,aceMonokai:i,aceLangTools:s,aceSearchbox:d,marked:v?l:null,katexCss:v?c:null,katex:v?p:null,markedKatex:v?m:null,mermaid:v?f:null}:{p5:b+"p5.min.js",pyodide:b+"pyodide/pyodide.js",pyodideIndex:b+"pyodide/",ace:b+"ace.min.js",acePython:b+"mode-python.min.js",aceMonokai:b+"theme-monokai.min.js",aceLangTools:b+"ext-language_tools.min.js",aceSearchbox:b+"ext-searchbox.min.js",marked:v?b+"marked.min.js":null,katexCss:v?b+"katex.min.css":null,katex:v?b+"katex.min.js":null,markedKatex:v?b+"marked-katex-extension.js":null,mermaid:v?b+"mermaid.min.js":null},x="pyfrilet:"+location.pathname,k=_.map((e,n)=>{const t="text/markdown"===e.getAttribute("type")?"markdown":"python",a=e.hasAttribute("data-hidden"),o=e.hasAttribute("data-readonly");let r=e.getAttribute("data-tab");null!==r||a||(r=1===_.length?"Code":`Bloc ${n+1}`);const i=e.textContent.replace(/^\n/,"");return{id:"tab-"+n,label:r,hidden:a,readonly:o,type:t,starterCode:i,code:i}}),E=e=>{try{return localStorage.getItem(e)}catch(e){return null}};let C;const S=E(x);let L=null;if(S)try{L=JSON.parse(S)}catch(e){L=null}if(L&&1===L.v&&Array.isArray(L.tabs)&&L.tabs.length>0){const e=e=>`${e.label}|${e.type}|${e.hidden?1:0}|${e.readonly?1:0}`;L.tabs.map(e).join(",")!==k.map(e).join(",")&&(L._stale=!0)}const j=!(!L||!L._stale);C=L&&1===L.v&&Array.isArray(L.tabs)&&L.tabs.length>0?L.tabs.map((e,n)=>{const t=k.find(n=>n.label===e.label&&n.type===e.type)||null;return{id:"tab-"+n,label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,starterCode:t?t.starterCode:e.content,code:e.content}}):k.map((e,n)=>{if(!e.hidden&&!e.readonly&&"python"===e.type){const t=e.label?e.label.replace(/[^a-zA-Z0-9]/g,"_"):String(n);let a=E(x+":"+t);if(a||"Code"!==e.label||1!==k.length||(a=E(x)),a&&a.trim())return{...e,code:a}}return e});const z=y.hasAttribute("data-no-watchdog");!function(e,t,a,o,r,i){e=e.slice();let s=i;const d=document.createElement("style");d.textContent=u,document.head.appendChild(d),document.body.innerHTML=h;const l=document.getElementById("pf-app"),c=document.getElementById("pf-drawer"),p=document.getElementById("pf-handle"),m=document.getElementById("pf-sketch"),f=document.getElementById("pf-viewport"),_=document.getElementById("pf-loader"),y=document.getElementById("pf-loader-msg"),g=document.getElementById("pf-err"),b=document.getElementById("pf-btn-run"),v=document.getElementById("pf-btn-code"),w=document.getElementById("pf-btn-dl"),x=document.getElementById("pf-btn-rec"),k=document.getElementById("pf-btn-reset"),E=document.getElementById("pf-btn-help"),C=document.getElementById("pf-grip"),S=document.getElementById("pf-handle-hint"),L=document.getElementById("pf-tabs"),j=document.getElementById("pf-markdown-view");let z=!1,I=Math.round(.56*window.innerHeight);function R(){document.documentElement.style.setProperty("--pf-drawer-h",I+"px")}function A(){z=!0,c.classList.add("pf-open"),v.classList.add("pf-active"),setTimeout(()=>{X(),q&&q.focus()},280)}function P(){z=!1,c.classList.remove("pf-open"),v.classList.remove("pf-active"),setTimeout(()=>{X();const e=J._p?.canvas;e&&e.removeAttribute("tabindex"),l.focus()},280)}function M(){z?P():A()}R();let T=null;const B=5,O=120,W=document.createElement("div");function D(e){if(e.target.closest(".pf-btn"))return;if(e.target.closest("#pf-grip"))return;const n=e.touches?e.touches[0].clientY:e.clientY;T={y:n,h:z?I:0,moved:!1},W.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function K(e){if(!T)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=T.y-n;if(Math.abs(t)>B&&(T.moved=!0),!T.moved)return;const a=Math.max(0,Math.min(window.innerHeight-50,T.h+t));a<O?(c.style.transition="none",c.style.height="32px"):(I=a,R(),z||A(),c.style.transition="none",c.style.height=I+"px"),X()}function $(e){if(!T)return;const n=T.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??T.y,a=T.y-t,o=T.h+a;T=null,W.style.display="none",document.body.style.userSelect="",c.style.transition="",c.style.height="",n&&(o<O?P():(I=Math.max(O,Math.min(window.innerHeight-50,o)),R(),z||A()),X())}Object.assign(W.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(W),C.addEventListener("click",e=>{e.stopPropagation(),M()}),p.addEventListener("mousedown",D,!0),document.addEventListener("mousemove",K),document.addEventListener("mouseup",$),p.addEventListener("touchstart",D,{passive:!1}),document.addEventListener("touchmove",K,{passive:!0}),document.addEventListener("touchend",$);let N=0,U=0;function F(e){g.textContent=e,g.style.display="block",A()}function H(){g.textContent="",g.style.display="none"}function Y(){if(!J._p||"fit"!==J._mode)return;const e=J._w,n=J._h;if(!e||!n)return;const t=l.clientWidth,a=l.clientHeight,o=Math.min(t/e,a/n);f.style.transform=`scale(${o})`}function X(){if("fullscreen"===J._mode?J.size("max"):Y(),G&&"function"==typeof G.windowResized)try{G.windowResized()}catch(e){F(String(e))}q&&q.resize()}window.addEventListener("mousemove",e=>{N=e.clientX,U=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(N=e.touches[0].clientX,U=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=J._p?J._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=J._w/n.width,a=J._h/n.height;return[(N-n.left)*t,(U-n.top)*a]},window.addEventListener("resize",X);let G=null;const J=new Proxy({_p:null,_mode:"fit",_w:0,_h:0,_setP(e){this._p=e},size(e,n,t){if(!this._p)return;const a=t??void 0;"max"===e||null==e?(this._mode="fullscreen",this._w=l.clientWidth,this._h=l.clientHeight,void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),f.style.transform="scale(1)"):(this._mode="fit",this._w=Math.max(1,0|e),this._h=Math.max(1,0|n),void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),Y())},noSmooth(){this._p?.noSmooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="pixelated")},smooth(){this._p?.smooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="auto")},sketchTitle(e){S.textContent=String(e)},getItem(e){try{return localStorage.getItem(e)}catch(e){return null}},storeItem(e,n){try{localStorage.setItem(e,String(n))}catch(e){}},removeItem(e){try{localStorage.removeItem(e)}catch(e){}},clearStorage(){try{localStorage.clear()}catch(e){}}},{get(e,n){if(n in e)return"function"==typeof e[n]?e[n].bind(e):e[n];if(e._p&&n in e._p){const t=e._p[n];return"function"==typeof t?t.bind(e._p):t}},set:(e,n,t)=>n.startsWith("_")?(e[n]=t,!0):(e._p&&(e._p[n]=t),!0)});function V(){if(Be(),G){try{G.remove()}catch(e){}G=null}m.innerHTML="",J._p=null,J._mode="fit",J._w=0,J._h=0,f.style.transform="scale(1)",S.textContent="Shift+Entrée → relancer  ·  Échap → ouvrir/fermer",ye&&(ye.destroy(),ye=null),he&&(he.destroy(),he=null),_e&&(_e.destroy(),_e=null),ge&&(ge.destroy(),ge=null),be&&(be.destroy(),be=null),ve&&(ve.destroy(),ve=null),we&&(we.destroy(),we=null),xe&&(xe.destroy(),xe=null),ke&&(ke.destroy(),ke=null),Ee&&(Ee.destroy(),Ee=null),Ce&&(Ce.destroy(),Ce=null),Se&&(Se.destroy(),Se=null),Le&&(Le.destroy(),Le=null),je&&(je.destroy(),je=null)}window.p5py=J;let q=null,Z=null;const Q={},ee=new Set;function ne(){L.innerHTML="",Z=null;const n=e.filter(e=>!e.hidden);L.style.display=n.length<=1?"none":"",n.forEach(e=>{const n=document.createElement("button");n.className="pf-tab",n.dataset.tabId=e.id,n.textContent=e.label,e.readonly&&n.classList.add("pf-tab-readonly"),"markdown"===e.type&&n.classList.add("pf-tab-markdown"),n.addEventListener("click",()=>te(e)),L.appendChild(n)}),n.length>0&&te(n[0],!0)}function te(e,n){if(n||Z!==e)if(Z=e,L.querySelectorAll(".pf-tab").forEach(n=>{n.classList.toggle("pf-tab-active",n.dataset.tabId===e.id)}),"markdown"===e.type){if(document.getElementById("pf-ace").style.display="none",j.style.display="block",window.marked){let n=marked.parse(e.starterCode);window.mermaid&&(n=n.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,(e,n)=>`<div class="mermaid">${n.replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">")}</div>`)),j.innerHTML=`<div class="pf-md-inner">${n}</div>`}else j.innerHTML=`<div class="pf-md-inner"><pre>${e.starterCode}</pre></div>`;window.mermaid&&mermaid.run({nodes:j.querySelectorAll(".mermaid")})}else document.getElementById("pf-ace").style.display="block",j.style.display="none",q&&Q[e.id]&&(q.setSession(Q[e.id]),q.setReadOnly(e.readonly),q.focus())}function ae(){let n=1;e.filter(e=>"python"===e.type).forEach(e=>{e.hidden||e.readonly||!Q[e.id]?n+=e.code.split("\n").length:(Q[e.id].setOption("firstLineNumber",n),n+=Q[e.id].getLength())})}function oe(){Object.keys(Q).forEach(e=>delete Q[e]),e.filter(e=>!e.hidden&&"python"===e.type).forEach(e=>{const n=ace.createEditSession(e.code,"ace/mode/python");if(n.setUseWorker(!1),n.setTabSize(4),Q[e.id]=n,!e.readonly){let e=null;n.on("change",()=>{null!==e&&(clearTimeout(e),ee.delete(e)),e=setTimeout(()=>{ee.delete(e),e=null,ie()},350),ee.add(e),ae(),de()})}});const n=e.find(e=>!e.hidden&&"python"===e.type);q&&n&&Q[n.id]&&(q.setSession(Q[n.id]),q.setReadOnly(n.readonly),q.renderer.updateFull(!0)),ae()}function re(){!o.ace.startsWith("vendor")&&o.ace.startsWith("http")||ace.config.set("basePath",o.ace.replace(/\/[^/]+$/,"/")),q=ace.edit("pf-ace"),q.setTheme("ace/theme/monokai"),q.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),q.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{q.completer?.popup?.isOpen||Ie()}}),q.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:P}),q.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:se}),q.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}}),oe(),ne(),de()}function ie(){const n={v:1,tabs:e.map(e=>({label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,content:e.hidden||e.readonly||"python"!==e.type||!Q[e.id]?e.code:Q[e.id].getValue()}))};try{localStorage.setItem(a,JSON.stringify(n))}catch(e){}}function se(){ie()}function de(){const n=s||e.some(e=>!e.hidden&&!e.readonly&&"python"===e.type&&Q[e.id]&&Q[e.id].getValue()!==e.starterCode);k.classList.toggle("pf-dirty",n)}function le(){ee.forEach(e=>clearTimeout(e)),ee.clear();try{localStorage.removeItem(a)}catch(e){}e.forEach(e=>{if(e.label)try{localStorage.removeItem(a+":"+e.label.replace(/[^a-zA-Z0-9]/g,"_"))}catch(e){}});try{localStorage.removeItem(a+":Code")}catch(e){}s=!1,e=t.map((e,n)=>({...e,id:"tab-"+n,code:e.starterCode})),oe(),ne(),de(),Ie()}window.addEventListener("beforeunload",se);let ce=null,pe=null;async function me(){return pe||(pe=(async()=>{const e={};if(o.pyodideIndex&&(e.indexURL=o.pyodideIndex),ce=await loadPyodide(e),ce.runPython("\nimport sys, types, js\nfrom js import p5py, _pfMouse\nfrom pyodide.ffi import JsProxy\n\n# ── Python builtins that must NOT be shadowed ──────────────────────\n_BLACKLIST = frozenset({\n 'abs','all','any','bin','bool','bytes','callable','chr','compile',\n 'delattr','dict','dir','divmod','enumerate','eval','exec',\n 'filter','float','format','frozenset','getattr','globals','hasattr',\n 'hash','help','hex','id','input','int','isinstance','issubclass',\n 'iter','len','list','locals','map','max','min','next','object',\n 'oct','open','ord','pow','print','property','range','repr',\n 'reversed','round','set','setattr','slice','sorted','staticmethod',\n 'str','sum','super','tuple','type','vars','zip',\n # p5 lifecycle hooks — user defines these, we don't import them\n 'setup','draw','preload',\n})\n\n# ── Introspect a hidden dummy p5 instance ─────────────────────────\n_dummy_node = js.document.createElement('div')\n_dummy = js.p5.new(lambda _: None, _dummy_node)\n\n_p5_functions = set() # names of callable JS members\n_p5_attributes = set() # names of scalar/readable members\n\nfor _n in dir(_dummy):\n if _n.startswith('_') or _n in _BLACKLIST:\n continue\n _v = getattr(_dummy, _n)\n if isinstance(_v, JsProxy):\n if callable(_v):\n _p5_functions.add(_n)\n # non-callable JsProxy (canvas, pixels…) → skip\n else:\n _p5_attributes.add(_n)\n\n# Read real initial values now, while dummy is still alive\n_attr_init = {}\nfor _n in _p5_attributes:\n try:\n _attr_init[_n] = getattr(_dummy, _n)\n except Exception:\n _attr_init[_n] = 0\n\n_dummy.remove()\ndel _dummy, _dummy_node\n\n# ── Build module ───────────────────────────────────────────────────\nm = types.ModuleType(\"p5\")\n\n# Generic function wrapper: delegates to live p5Bridge instance\nclass _FW:\n __slots__ = ('_n',)\n def __init__(self, n): self._n = n\n def __call__(self, *a): return getattr(p5py, self._n)(*a)\n def __repr__(self): return f'<p5 function {self._n}>'\n\nfor _n in _p5_functions:\n setattr(m, _n, _FW(_n))\n\n# ── Special overrides (our bridge has custom behaviour) ────────────\n# smooth/noSmooth exist on a real p5 instance so introspection finds\n# them — but our Proxy overrides them to also toggle CSS image-rendering.\n# size and sketchTitle are pyfrilet-only: NOT on a real p5 instance,\n# so introspection misses them — add them explicitly.\nfor _n in ('sketchTitle',):\n setattr(m, _n, _FW(_n))\n _p5_functions.add(_n) # keep __all__ consistent\n\n# size() calls _pf_refresh after resizing so width/height are immediately\n# correct in setup() — consistent with p5.js JS behaviour.\nclass _SizeWrapper:\n def __call__(self, *a):\n p5py.size(*a)\n _pf_refresh(_ns_ref[0])\n return _GetCanvasWrapper()()\n def __repr__(self): return '<p5 function size>'\nsetattr(m, 'size', _SizeWrapper())\nsetattr(m, 'createCanvas', m.size) # alias — createCanvas(...) == size(...)\n_p5_functions.add('size')\n_p5_functions.add('createCanvas')\n_ns_ref = [{}] # filled in by runCode before each exec\n\n# getCanvas() — returns the p5.Element wrapping the canvas,\n# so the user can call .drop(create_proxy(fn)), .mouseOver(), etc. directly like in JS.\nclass _GetCanvasWrapper:\n def __call__(self):\n p = p5py._p\n if p is None:\n raise RuntimeError('getCanvas() doit être appelé dans setup() ou après')\n p.canvas.id = '__pf_canvas__'\n return p.select('#__pf_canvas__')\n def __repr__(self): return '<p5 function getCanvas>'\nsetattr(m, 'getCanvas', _GetCanvasWrapper())\n_p5_functions.add('getCanvas')\n\n# mouseX / mouseY: override with our accurate coordinate calculator\n# (p5's own values are wrong when a CSS-transformed parent is used)\n_MOUSE_OVERRIDE = frozenset({'mouseX', 'mouseY'})\n\n# Initial values from the dummy instance — constants like WEBGL, DEGREES,\n# LEFT_ARROW… are correct from the very first setup() call.\nfor _n in _p5_attributes:\n if _n in _MOUSE_OVERRIDE:\n setattr(m, _n, 0.0)\n else:\n setattr(m, _n, _attr_init.get(_n, 0))\n\n# Build __all__ for import * — done later, after snake_case aliases are added\n\n# ── _pf_refresh: called before every event callback ───────────────\nimport re as _re\n\n# Pre-compute snake_case alias for each attribute — None if identical\n_attr_snake = {\n _k: (_re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _k) or None)\n for _k in _p5_attributes\n}\n_attr_snake = {_k: (_s if _s != _k else None) for _k, _s in _attr_snake.items()}\n\n# Add snake_case names to _p5_attributes so __all__ and _pf_refresh cover them\nfor _k, _sk in list(_attr_snake.items()):\n if _sk:\n _p5_attributes.add(_sk)\n setattr(m, _sk, getattr(m, _k, 0)) # initial value mirrors camelCase\n _attr_snake[_sk] = None # snake name has no further alias\n\ndef _pf_refresh(ns):\n # accurate mouse coords (bypasses p5's stale CSS-transform offset)\n mx, my = _pfMouse()\n\n # update all known scalar attributes from live instance\n for _k in _p5_attributes:\n _sk = _attr_snake.get(_k)\n if _k in _MOUSE_OVERRIDE:\n _v = mx if _k in ('mouseX', 'mouse_x') else my\n elif _sk is None and _k not in _attr_snake:\n # pure snake_case entry — skip, updated via its camelCase counterpart\n continue\n else:\n try:\n _v = getattr(p5py, _k)\n except Exception:\n continue\n setattr(m, _k, _v)\n if _k in ns:\n ns[_k] = _v\n if _sk:\n setattr(m, _sk, _v)\n if _sk in ns:\n ns[_sk] = _v\n\nsys.modules[\"p5\"] = m\n\n# ── draw() watchdog via sys.settrace ──────────────────────────────\n# Trace is called on every Python line event. We only call time.monotonic()\n# every N events to minimize overhead — a tight loop still triggers within\n# a few microseconds, so detection latency is negligible.\nimport time as _time\n\n_WDOG_CHECK_EVERY = 100\n_wdog_deadline = [0.0]\n_wdog_count = [0]\n\ndef _wdog_trace(frame, event, arg):\n _wdog_count[0] += 1\n if _wdog_count[0] >= _WDOG_CHECK_EVERY:\n _wdog_count[0] = 0\n if _time.monotonic() > _wdog_deadline[0]:\n raise TimeoutError(\"draw() watchdog\")\n return _wdog_trace\n\ndef _pf_draw_watchdog(fn, timeout_ms):\n _wdog_count[0] = 0\n _wdog_deadline[0] = _time.monotonic() + timeout_ms * 0.001\n sys.settrace(_wdog_trace)\n try:\n fn()\n finally:\n sys.settrace(None)\n\ndef _pf_draw_direct(fn, timeout_ms):\n fn()\n\ndef _snake_to_camel(name):\n parts = name.split('_')\n return parts[0] + ''.join(p.capitalize() for p in parts[1:])\n\n# Pre-populate snake_case aliases so \"from p5 import no_fill\" works\nfor _camel in list(vars(m).keys()):\n _snake = _re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _camel)\n if _snake != _camel and not hasattr(m, _snake):\n setattr(m, _snake, getattr(m, _camel))\n if _camel in _p5_functions:\n _p5_functions.add(_snake)\n\n# Rebuild __all__ now that snake_case aliases are included\nm.__all__ = sorted(_p5_functions | _p5_attributes)\n\ndef _p5_getattr(name):\n camel = _snake_to_camel(name)\n if camel != name:\n val = getattr(m, camel, None)\n if val is not None:\n return val\n raise AttributeError(f\"module 'p5' has no attribute '{name}'\")\n\nm.__getattr__ = _p5_getattr\n"),q){fe(ce.runPython("list(m.__all__)").toJs())}})(),pe)}function fe(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,a,o,r){r(null,o.length>0?n:[])}},a=ace.require("ace/ext/language_tools");a&&Array.isArray(a.completers)&&(a.completers=a.completers.filter(e=>!0!==e._pyfrilet)),t._pyfrilet=!0,q.completers=[...q.completers||[],t]}let ue=!1,he=null,_e=null,ye=null,ge=null,be=null,ve=null,we=null,xe=null,ke=null,Ee=null,Ce=null,Se=null,Le=null,je=null;const ze=300;async function Ie(){if(ue)return;ue=!0,b.classList.add("pf-running"),H(),V(),ce||(y.textContent="Initialisation de Pyodide…",_.style.display="flex");try{await me()}catch(e){return _.style.display="none",F("Erreur Pyodide : "+e),ue=!1,void b.classList.remove("pf-running")}_.style.display="none";const t=e.filter(e=>"python"===e.type).map(e=>e.hidden||e.readonly||!Q[e.id]?e.code:Q[e.id].getValue()).join("\n");try{y.textContent="Chargement des dépendances…",_.style.display="flex",await ce.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:n})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}_.style.display="none",ce.globals.set("_USER_CODE",t);try{ce.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)"),ce.runPython("_ns_ref[0] = _ns")}catch(e){return F(String(e)),ue=!1,void b.classList.remove("pf-running")}let a,o,i,s,d,l,c,p,f,u,h,g,v,w;try{const e=(e,n)=>ce.runPython(`_ns.get('${e}') or _ns.get('${n}')`);d=e("preload","preload"),a=e("setup","setup"),o=e("draw","draw"),i=e("mousePressed","mouse_pressed"),s=e("keyPressed","key_pressed"),l=e("mouseDragged","mouse_dragged"),c=e("mouseReleased","mouse_released"),p=e("mouseMoved","mouse_moved"),f=e("mouseWheel","mouse_wheel"),u=e("doubleClicked","double_clicked"),h=e("keyReleased","key_released"),g=e("touchStarted","touch_started"),v=e("touchMoved","touch_moved"),w=e("touchEnded","touch_ended")}catch(e){return F(String(e)),ue=!1,void b.classList.remove("pf-running")}if(!o)return F("Le script doit définir au moins une fonction draw()."),ue=!1,void b.classList.remove("pf-running");const{create_proxy:x}=ce.pyimport("pyodide.ffi"),k=ce.runPython("_ns.get('windowResized')"),E=ce.globals.get("_pf_refresh"),C=ce.globals.get(r?"_pf_draw_direct":"_pf_draw_watchdog"),S=ce.globals.get("_ns"),L=e=>e?x(()=>{try{E(S),e()}catch(e){F(String(e))}}):null;ye=d?x(()=>{try{d()}catch(e){F(String(e))}}):null,he=a?x(()=>{try{a()}catch(e){F(String(e))}}):null,_e=x(()=>{try{E(S),C(o,ze)}catch(e){const n=String(e);V(),n.includes("TimeoutError")||n.includes("watchdog")?F(`draw() a dépassé ${ze}ms — sketch arrêté (watchdog).`):F(n)}}),ge=L(i),be=L(c),ve=L(l),we=L(p),xe=L(f),ke=L(u),Ee=L(s),Ce=L(h),Se=L(g),Le=L(v),je=L(w);const j=k?x(()=>{try{k()}catch(e){F(String(e))}}):null;let z=!1;G=new p5(e=>{J._setP(e),ye&&(e.preload=()=>{ye()}),e.setup=()=>{he&&he(),e.canvas||J.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),z=!0},e.draw=()=>{z&&_e()},e.mousePressed=()=>{z&&ge&&ge()},e.mouseReleased=()=>{z&&be&&be()},e.mouseDragged=()=>{z&&ve&&ve()},e.mouseMoved=()=>{z&&we&&we()},e.mouseWheel=e=>{z&&xe&&xe()},e.doubleClicked=()=>{z&&ke&&ke()},e.keyPressed=()=>{z&&Ee&&Ee()},e.keyReleased=()=>{z&&Ce&&Ce()},Se&&(e.touchStarted=()=>{z&&Se()}),Le&&(e.touchMoved=()=>{z&&Le()}),je&&(e.touchEnded=()=>{z&&je()}),e.windowResized=()=>{"fullscreen"===J._mode?J.size("max"):Y(),j&&j()}},m),ue=!1,b.classList.remove("pf-running")}const Re='<!doctype html>\n<html lang="fr">\n<head>\n <meta charset="utf-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <title>export</title>\n <script src="https://cdn.jsdelivr.net/npm/pyfrilet@0.5.3/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\nFILLME-SCRIPTS\n\n</body>\n</html>';function Ae(){const n=e.map((e,n)=>{let t;t="python"!==e.type||e.hidden||e.readonly||!Q[e.id]?e.code:Q[e.id].getValue();const a=[],o="markdown"===e.type?"text/markdown":"text/python";null!==e.label&&a.push(`data-tab="${e.label.replace(/"/g,"&quot;")}"`),e.hidden&&a.push("data-hidden"),e.readonly&&a.push("data-readonly");return`<script type="${o}"${a.length?" "+a.join(" "):""}>\n${t.replace(/<\/script>/gi,"<\\/script>")}\n<\/script>`}).join("\n\n"),t=Re.replace("FILLME-SCRIPTS",n),a=new Blob([t],{type:"text/html;charset=utf-8"}),o=URL.createObjectURL(a),r=Object.assign(document.createElement("a"),{href:o,download:"sketch.html"});document.body.appendChild(r),r.click(),document.body.removeChild(r),URL.revokeObjectURL(o)}let Pe=null,Me=[];function Te(){const e=J._p?.canvas;if(!e)return;const n=["video/webm;codecs=vp9","video/webm;codecs=vp8","video/webm"].find(e=>MediaRecorder.isTypeSupported(e))||"video/webm",t=e.captureStream();Pe=new MediaRecorder(t,{mimeType:n}),Me=[],Pe.ondataavailable=e=>{e.data.size&&Me.push(e.data)},Pe.onstop=()=>{const e=new Blob(Me,{type:n}),t=URL.createObjectURL(e),a=n.includes("webm")?"webm":"mp4";Object.assign(document.createElement("a"),{href:t,download:`sketch.${a}`}).click(),URL.revokeObjectURL(t),x.textContent="⏺",x.title="Enregistrer WebM",x.classList.remove("pf-recording"),Pe=null},Pe.start(),x.textContent="⏹",x.title="Arrêter l'enregistrement",x.classList.add("pf-recording")}function Be(){Pe&&"inactive"!==Pe.state&&Pe.stop()}x.addEventListener("click",()=>{Pe?Be():Te()}),b.addEventListener("click",()=>Ie()),v.addEventListener("click",()=>{z?P():(I=window.innerHeight-32,R(),A())}),w.addEventListener("click",Ae);const Oe="https://codeberg.org/nopid/pyfrilet";function We(e){return new Promise((n,t)=>{const a=document.createElement("script");a.src=e,a.onload=n,a.onerror=()=>t(new Error("Impossible de charger : "+e)),document.head.appendChild(a)})}E.addEventListener("click",()=>window.open(Oe,"_blank")),k.addEventListener("click",()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}),window.addEventListener("keydown",e=>{const n=z&&q&&q.isFocused&&q.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void Ie();if("Escape"===e.key){const t=document.querySelector(".ace_search");if(t&&"none"!==t.style.display)return e.preventDefault(),e.stopPropagation(),q.searchBox?q.searchBox.hide():t.style.display="none",void q.focus();if(n){const n=q.completer?.popup?.isOpen;if(n)return;return e.preventDefault(),e.stopPropagation(),void P()}return e.preventDefault(),e.stopPropagation(),void(z?P():A())}if(!n)return"s"!==e.key&&"S"!==e.key||!e.ctrlKey&&!e.metaKey?"r"!==e.key&&"R"!==e.key||!e.ctrlKey&&!e.metaKey||e.altKey?void 0:(e.preventDefault(),void(confirm("Réinitialiser ? Les modifications seront perdues.")&&le())):(e.preventDefault(),void se())}else e.preventDefault()},!0),(async()=>{y.textContent="Chargement des dépendances…",_.style.display="flex";try{if(await We(o.p5),o.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=o.katexCss,document.head.appendChild(e),await We(o.marked),await We(o.katex),await We(o.markedKatex),await We(o.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"neutral"})}await We(o.ace),await We(o.acePython),await We(o.aceMonokai),await We(o.aceLangTools),await We(o.aceSearchbox),await We(o.pyodide)}catch(e){return y.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}re(),await Ie(),_.style.display="none"})()}(C,k,x,w,z,j)})}();
1
+ !function(){"use strict";const e=document.currentScript;let n=!1;const t="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",a="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js",r="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js",o="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",i="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js",s="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js",d="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",l="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js",c="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css",p="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js",m="https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js",f="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js",u="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css",_="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js",h="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js",y="https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.8.0/lib/addon-unicode11.min.js",b="html, body {\n height: 100%; margin: 0; overflow: hidden;\n background: #111;\n}\n#pf-root {\n position: fixed; inset: 0;\n display: flex; flex-direction: column;\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n}\n\n/* ── app area ── */\n#pf-app:focus { outline: none; }\n#pf-app {\n flex: 1; min-height: 0;\n position: relative;\n background: #111;\n display: flex; align-items: center; justify-content: center;\n overflow: hidden;\n}\n#pf-viewport {\n transform-origin: 50% 50%;\n will-change: transform;\n}\n#pf-viewport canvas {\n display: block;\n outline: none;\n}\n#pf-loader {\n position: absolute; inset: 0;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n gap: 14px;\n background: #111;\n color: #565f89;\n font-size: 13px;\n z-index: 50;\n pointer-events: none;\n}\n#pf-loader-bar {\n width: 160px; height: 2px;\n background: #2a2c3e;\n border-radius: 2px;\n overflow: hidden;\n}\n#pf-loader-bar::after {\n content: '';\n display: block;\n height: 100%;\n width: 40%;\n background: #7aa2f7;\n border-radius: 2px;\n animation: pf-slide 1.2s ease-in-out infinite;\n}\n@keyframes pf-slide {\n 0% { transform: translateX(-100%); }\n 100% { transform: translateX(350%); }\n}\n\n/* ── drawer (slide-up editor panel) ── */\n#pf-drawer {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n background: #1a1b26;\n height: 32px; /* collapsed = handle only */\n transition: height 0.26s cubic-bezier(.4, 0, .2, 1);\n overflow: hidden;\n /* shadow cast upward onto the app */\n box-shadow: 0 -4px 20px rgba(0,0,0,.55);\n}\n#pf-drawer.pf-open {\n height: var(--pf-drawer-h, 56vh);\n}\n\n/* ── handle bar ── */\n#pf-handle {\n height: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n padding: 0 8px 0 6px;\n background: #24283b;\n border-top: 1px solid #3d4166;\n cursor: ns-resize;\n user-select: none;\n gap: 6px;\n flex-shrink: 0;\n}\n/* grip zone: clickable to toggle, draggable to resize */\n#pf-grip {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 5px 6px;\n flex-shrink: 0;\n opacity: .5;\n border-radius: 4px;\n transition: opacity .15s, background .15s;\n cursor: pointer;\n}\n#pf-grip:hover { opacity: .85; background: rgba(255,255,255,.06); }\n#pf-grip span {\n display: block;\n width: 16px; height: 2px;\n background: #a9b1d6;\n border-radius: 1px;\n}\n#pf-handle-hint {\n flex: 1;\n color: #565f89;\n font-size: 10px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n#pf-handle-btns {\n display: flex;\n gap: 4px;\n flex-shrink: 0;\n}\n.pf-btn {\n height: 26px;\n min-width: 26px;\n padding: 0 5px;\n border: 0; border-radius: 5px;\n cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n font-size: 13px; line-height: 1;\n white-space: nowrap;\n transition: background .15s, transform .1s, opacity .15s;\n outline: none;\n box-sizing: border-box;\n}\n.pf-btn:active { transform: scale(.88); }\n.pf-btn:focus-visible { outline: 2px solid #7aa2f7; outline-offset: 1px; }\n\n#pf-btn-run { background: #1a6b3a; color: #9ece6a; font-size: 11px; }\n#pf-btn-run:hover { background: #1f8447; color: #b9f27a; }\n#pf-btn-run.pf-running { opacity: .5; cursor: not-allowed; }\n\n#pf-btn-code { background: #2a2c3e; color: #7aa2f7; font-size: 14px; }\n#pf-btn-code:hover { background: #3d4166; color: #c0caf5; }\n#pf-btn-code.pf-active { background: #3d4166; color: #e0af68; }\n\n#pf-btn-dl { background: #2a2c3e; color: #9d7cd8; font-size: 14px; }\n#pf-btn-dl:hover { background: #3d4166; color: #bb9af7; }\n\n#pf-btn-rec { background: #2a2c3e; color: #f7768e; font-size: 13px; }\n#pf-btn-rec:hover { background: #3d4166; color: #ff9e9e; }\n#pf-btn-rec.pf-recording { background: #6b1a1a; color: #f7768e; animation: pf-blink .8s step-end infinite; }\n@keyframes pf-blink { 50% { opacity: .4; } }\n\n#pf-btn-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }\n#pf-btn-reset:hover { background: #3d4166; color: #ffc777; }\n#pf-btn-reset.pf-dirty::after {\n content: '●';\n position: absolute;\n top: 2px; right: 3px;\n font-size: 7px;\n color: #e0af68;\n line-height: 1;\n}\n#pf-btn-reset { position: relative; }\n\n/* ── editor area inside drawer ── */\n#pf-editor-wrap {\n flex: 1;\n min-height: 80px;\n position: relative;\n display: flex;\n flex-direction: column;\n}\n#pf-ace { flex: 1; position: relative; min-height: 0; }\n\n/* ── tab bar ── */\n#pf-tabs {\n display: flex;\n flex-shrink: 0;\n background: #1a1b2e;\n border-bottom: 1px solid #414868;\n overflow-x: auto;\n scrollbar-width: none;\n}\n#pf-tabs:empty { display: none; }\n.pf-tab {\n padding: 5px 14px;\n font-size: 12px;\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n color: #737aa2;\n cursor: pointer;\n white-space: nowrap;\n transition: color .15s, border-color .15s;\n}\n.pf-tab:hover { color: #c0caf5; }\n.pf-tab.pf-tab-active { color: #c0caf5; border-bottom-color: #7aa2f7; }\n.pf-tab.pf-tab-readonly::after { content: ' 🔒'; font-size: 10px; opacity: .6; }\n.pf-tab.pf-tab-markdown::after { content: ' ✎'; font-size: 11px; opacity: .6; }\n\n/* ── markdown view ── */\n@import url('https://fonts.googleapis.com/css2?family=Alegreya+Sans:ital,wght@0,400;0,700;1,400&family=Fira+Code:wght@300..700&display=swap');\n\n#pf-markdown-view {\n flex: 1;\n overflow: auto;\n background: #f4f4f0;\n}\n\n#pf-markdown-view .pf-md-inner {\n width: 100%;\n max-width: 680px;\n margin: 0 auto;\n padding: 48px 48px 72px;\n box-sizing: border-box;\n font-family: 'Alegreya Sans', Georgia, serif;\n font-size: 17px;\n line-height: 1.8;\n color: #1c1c2e;\n}\n\n#pf-markdown-view h1 {\n font-size: 2.1em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 0 0 .3em;\n padding-bottom: .3em;\n border-bottom: 2px solid #d8d8e8;\n line-height: 1.2;\n}\n#pf-markdown-view h2 {\n font-size: 1.4em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 2em 0 .5em;\n padding-bottom: .2em;\n border-bottom: 1px solid #e0e0ec;\n}\n#pf-markdown-view h3 {\n font-size: 1.1em;\n font-weight: 700;\n color: #2a2a4a;\n margin: 1.6em 0 .4em;\n}\n\n#pf-markdown-view p { margin: .75em 0; }\n#pf-markdown-view ul,\n#pf-markdown-view ol { padding-left: 1.6em; margin: .75em 0; }\n#pf-markdown-view li { margin: .3em 0; }\n#pf-markdown-view hr { border: none; border-top: 1px solid #dde; margin: 2em 0; }\n#pf-markdown-view blockquote {\n margin: 1em 0;\n padding: .5em 1em;\n border-left: 3px solid #aab;\n color: #555;\n background: #ededf5;\n border-radius: 0 4px 4px 0;\n}\n\n#pf-markdown-view code {\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n font-size: .84em;\n background: #e8e8f2;\n color: #3a3a6a;\n padding: .15em .45em;\n border-radius: 4px;\n}\n#pf-markdown-view pre {\n background: #1a1b2e;\n border-radius: 8px;\n padding: 1em 1.2em;\n overflow: auto;\n margin: 1.2em 0;\n box-shadow: 0 2px 8px rgba(0,0,0,.12);\n}\n#pf-markdown-view pre code {\n background: transparent;\n color: #c0caf5;\n font-size: .86em;\n padding: 0;\n line-height: 1.6;\n border-radius: 0;\n}\n\n#pf-markdown-view table {\n border-collapse: collapse;\n width: 100%;\n margin: 1.2em 0;\n font-size: .95em;\n}\n#pf-markdown-view th {\n background: #e4e4f0;\n color: #1c1c2e;\n font-weight: 700;\n text-align: left;\n padding: .55em .85em;\n border: 1px solid #d0d0e8;\n}\n#pf-markdown-view td {\n padding: .5em .85em;\n border: 1px solid #e0e0ee;\n vertical-align: top;\n}\n#pf-markdown-view tr:nth-child(even) td { background: #f0f0f8; }\n\n#pf-markdown-view a {\n color: #3a5fc8;\n text-decoration: none;\n border-bottom: 1px solid rgba(58,95,200,.3);\n transition: color .15s, border-color .15s;\n}\n#pf-markdown-view a:hover { color: #1a3fa0; border-bottom-color: #1a3fa0; }\n\n#pf-markdown-view .katex-display {\n overflow-x: auto;\n padding: .5em 0;\n margin: 1.2em 0;\n}\n#pf-markdown-view .mermaid {\n text-align: center;\n margin: 1.5em 0;\n background: #ededf5;\n border-radius: 8px;\n padding: 1em;\n}\n\n/* ── error panel (below editor, never overlaps ACE) ── */\n#pf-err {\n flex-shrink: 0;\n max-height: 120px;\n overflow: auto;\n margin: 0; padding: 8px 13px;\n font-size: 11.5px; line-height: 1.45;\n background: rgba(13, 3, 3, .95);\n color: #f7768e;\n white-space: pre-wrap;\n display: none;\n border-top: 1px solid rgba(247, 118, 142, .35);\n}\n/* ── xterm terminal ── */\n#pf-xterm {\n display: none;\n position: absolute;\n inset: 0;\n padding: 10px 12px;\n box-sizing: border-box;\n background: #000000;\n overflow: hidden;\n}\n\n#pf-xterm.pf-xterm-overlay {\n background: rgba(0, 0, 0, 0.82);\n}\n\n/* xterm interne : prendre toute la hauteur */\n#pf-xterm .xterm {\n height: 100%;\n}\n#pf-xterm .xterm-screen {\n height: 100% !important;\n}\n",g='<div id="pf-root">\n <div id="pf-app" tabindex="-1">\n <div id="pf-viewport"><div id="pf-sketch"></div></div>\n <div id="pf-xterm"></div>\n <div id="pf-loader">\n <span id="pf-loader-msg">Chargement…</span>\n <div id="pf-loader-bar"></div>\n </div>\n </div>\n <div id="pf-drawer">\n <div id="pf-handle">\n <div id="pf-grip" title="Clic → ouvrir/fermer"><span></span><span></span><span></span></div>\n <span id="pf-handle-hint">Clic ☰ → ouvrir/fermer &nbsp;·&nbsp; Shift+Entrée → relancer</span>\n <div id="pf-handle-btns">\n <button class="pf-btn" id="pf-btn-run" title="Relancer (Shift+Entrée)">&#9654;</button>\n <button class="pf-btn" id="pf-btn-code" title="Éditeur plein écran">&#9999;&#xFE0F;</button>\n <button class="pf-btn" id="pf-btn-dl" title="Télécharger HTML autonome">&#128190;</button>\n <button class="pf-btn" id="pf-btn-rec" title="Enregistrer WebM">⏺</button>\n <button class="pf-btn" id="pf-btn-help" title="Aide">?</button>\n <button class="pf-btn" id="pf-btn-reset" title="Réinitialiser le code (Ctrl+R)">&#8635;</button>\n </div>\n </div>\n <div id="pf-editor-wrap">\n <div id="pf-tabs"></div>\n <div id="pf-markdown-view" style="display:none"></div>\n <div id="pf-ace"></div>\n </div>\n <pre id="pf-err"></pre>\n </div>\n</div>';document.addEventListener("DOMContentLoaded",function(){const w=[...document.querySelectorAll('script[type="text/python"], script[type="text/markdown"], python')];if(0===w.length)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const v=e||w[0],x=(v.getAttribute("data-sources")||v.getAttribute("sources")||"cdn").toLowerCase().trim(),k=(v.getAttribute("data-vendor")||v.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");n="cdn"===x;const E=w.some(e=>"text/markdown"===e.getAttribute("type")),C=n?{p5:t,pyodide:a,pyodideIndex:null,ace:r,acePython:o,aceMonokai:i,aceLangTools:s,aceSearchbox:d,marked:E?l:null,katexCss:E?c:null,katex:E?p:null,markedKatex:E?m:null,mermaid:E?f:null,xtermCss:u,xterm:_,xtermFit:h,xtermUni:y}:{p5:k+"p5.min.js",pyodide:k+"pyodide/pyodide.js",pyodideIndex:k+"pyodide/",ace:k+"ace.min.js",acePython:k+"mode-python.min.js",aceMonokai:k+"theme-monokai.min.js",aceLangTools:k+"ext-language_tools.min.js",aceSearchbox:k+"ext-searchbox.min.js",marked:E?k+"marked.min.js":null,katexCss:E?k+"katex.min.css":null,katex:E?k+"katex.min.js":null,markedKatex:E?k+"marked-katex-extension.js":null,mermaid:E?k+"mermaid.min.js":null,xtermCss:k+"xterm.min.css",xterm:k+"xterm.min.js",xtermFit:k+"xterm-addon-fit.min.js",xtermUni:k+"addon-unicode11.min.js"},S="pyfrilet:"+location.pathname,j=w.map((e,n)=>{const t="text/markdown"===e.getAttribute("type")?"markdown":"python",a=e.hasAttribute("data-hidden"),r=e.hasAttribute("data-readonly");let o=e.getAttribute("data-tab");null!==o||a||(o=1===w.length?"Code":`Bloc ${n+1}`);const i=e.textContent.replace(/^\n/,"");return{id:"tab-"+n,label:o,hidden:a,readonly:r,type:t,starterCode:i,code:i}}),L=e=>{try{return localStorage.getItem(e)}catch(e){return null}};let z;const R=L(S);let I=null;if(R)try{I=JSON.parse(R)}catch(e){I=null}if(I&&1===I.v&&Array.isArray(I.tabs)&&I.tabs.length>0){const e=e=>`${e.label}|${e.type}|${e.hidden?1:0}|${e.readonly?1:0}`;I.tabs.map(e).join(",")!==j.map(e).join(",")&&(I._stale=!0)}const T=!(!I||!I._stale);z=I&&1===I.v&&Array.isArray(I.tabs)&&I.tabs.length>0?I.tabs.map((e,n)=>{const t=j.find(n=>n.label===e.label&&n.type===e.type)||null;return{id:"tab-"+n,label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,starterCode:t?t.starterCode:e.content,code:e.content}}):j.map((e,n)=>{if(!e.hidden&&!e.readonly&&"python"===e.type){const t=e.label?e.label.replace(/[^a-zA-Z0-9]/g,"_"):String(n);let a=L(S+":"+t);if(a||"Code"!==e.label||1!==j.length||(a=L(S)),a&&a.trim())return{...e,code:a}}return e});const A=v.hasAttribute("data-no-watchdog");!function(e,t,a,r,o,i){e=e.slice();let s=i;const d=document.createElement("style");d.textContent=b,document.head.appendChild(d),document.body.innerHTML=g;const l=document.getElementById("pf-app"),c=document.getElementById("pf-drawer"),p=document.getElementById("pf-handle"),m=document.getElementById("pf-sketch"),f=document.getElementById("pf-viewport"),u=document.getElementById("pf-loader"),_=document.getElementById("pf-loader-msg"),h=document.getElementById("pf-err"),y=document.getElementById("pf-btn-run"),w=document.getElementById("pf-btn-code"),v=document.getElementById("pf-btn-dl"),x=document.getElementById("pf-btn-rec"),k=document.getElementById("pf-btn-reset"),E=document.getElementById("pf-btn-help"),C=document.getElementById("pf-grip"),S=document.getElementById("pf-handle-hint"),j=document.getElementById("pf-tabs"),L=document.getElementById("pf-markdown-view");let z=!1,R=Math.round(.56*window.innerHeight);function I(){document.documentElement.style.setProperty("--pf-drawer-h",R+"px")}function T(){z=!0,c.classList.add("pf-open"),w.classList.add("pf-active"),setTimeout(()=>{G(),q&&q.focus()},280)}function A(){z=!1,c.classList.remove("pf-open"),w.classList.remove("pf-active"),setTimeout(()=>{G();const e=V._p?.canvas;e&&e.removeAttribute("tabindex"),l.focus()},280)}function P(){z?A():T()}I();let M=null;const B=5,O=120,W=document.createElement("div");function F(e){if(e.target.closest(".pf-btn"))return;if(e.target.closest("#pf-grip"))return;const n=e.touches?e.touches[0].clientY:e.clientY;M={y:n,h:z?R:0,moved:!1},W.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function D(e){if(!M)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=M.y-n;if(Math.abs(t)>B&&(M.moved=!0),!M.moved)return;const a=Math.max(0,Math.min(window.innerHeight-50,M.h+t));a<O?(c.style.transition="none",c.style.height="32px"):(R=a,I(),z||T(),c.style.transition="none",c.style.height=R+"px"),G()}function N(e){if(!M)return;const n=M.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??M.y,a=M.y-t,r=M.h+a;M=null,W.style.display="none",document.body.style.userSelect="",c.style.transition="",c.style.height="",n&&(r<O?A():(R=Math.max(O,Math.min(window.innerHeight-50,r)),I(),z||T()),G())}Object.assign(W.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(W),C.addEventListener("click",e=>{e.stopPropagation(),P()}),p.addEventListener("mousedown",F,!0),document.addEventListener("mousemove",D),document.addEventListener("mouseup",N),p.addEventListener("touchstart",F,{passive:!1}),document.addEventListener("touchmove",D,{passive:!0}),document.addEventListener("touchend",N);let U=0,K=0;function $(e){h.textContent=e,h.style.display="block",T()}function H(){h.textContent="",h.style.display="none"}function Y(){if(!V._p||"fit"!==V._mode)return;const e=V._w,n=V._h;if(!e||!n)return;const t=l.clientWidth,a=l.clientHeight,r=Math.min(t/e,a/n);f.style.transform=`scale(${r})`}function G(){if("fullscreen"===V._mode?V.size("max"):Y(),J&&"function"==typeof J.windowResized)try{J.windowResized()}catch(e){$(String(e))}q&&q.resize()}window.addEventListener("mousemove",e=>{U=e.clientX,K=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(U=e.touches[0].clientX,K=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=V._p?V._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=V._w/n.width,a=V._h/n.height;return[(U-n.left)*t,(K-n.top)*a]},window.addEventListener("resize",G);let J=null;const V=new Proxy({_p:null,_mode:"fit",_w:0,_h:0,_setP(e){this._p=e},size(e,n,t){if(!this._p)return;const a=t??void 0;"max"===e||null==e?(this._mode="fullscreen",this._w=l.clientWidth,this._h=l.clientHeight,void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),f.style.transform="scale(1)"):(this._mode="fit",this._w=Math.max(1,0|e),this._h=Math.max(1,0|n),void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),Y())},noSmooth(){this._p?.noSmooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="pixelated")},smooth(){this._p?.smooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="auto")},sketchTitle(e){S.textContent=String(e)},getItem(e){try{return localStorage.getItem(e)}catch(e){return null}},storeItem(e,n){try{localStorage.setItem(e,String(n))}catch(e){}},removeItem(e){try{localStorage.removeItem(e)}catch(e){}},clearStorage(){try{localStorage.clear()}catch(e){}}},{get(e,n){if(n in e)return"function"==typeof e[n]?e[n].bind(e):e[n];if(e._p&&n in e._p){const t=e._p[n];return"function"==typeof t?t.bind(e._p):t}},set:(e,n,t)=>n.startsWith("_")?(e[n]=t,!0):(e._p&&(e._p[n]=t),!0)});function X(){if(Fe(),J){try{J.remove()}catch(e){}J=null}m.innerHTML="",V._p=null,V._mode="fit",V._w=0,V._h=0,f.style.transform="scale(1)",S.textContent="Shift+Entrée → relancer  ·  Échap → ouvrir/fermer",be&&(be.destroy(),be=null),he&&(he.destroy(),he=null),ye&&(ye.destroy(),ye=null),ge&&(ge.destroy(),ge=null),we&&(we.destroy(),we=null),ve&&(ve.destroy(),ve=null),xe&&(xe.destroy(),xe=null),ke&&(ke.destroy(),ke=null),Ee&&(Ee.destroy(),Ee=null),Ce&&(Ce.destroy(),Ce=null),Se&&(Se.destroy(),Se=null),je&&(je.destroy(),je=null),Le&&(Le.destroy(),Le=null),ze&&(ze.destroy(),ze=null)}window.p5py=V;let q=null,Z=null;const Q={},ee=new Set;function ne(){j.innerHTML="",Z=null;const n=e.filter(e=>!e.hidden);j.style.display=n.length<=1?"none":"",n.forEach(e=>{const n=document.createElement("button");n.className="pf-tab",n.dataset.tabId=e.id,n.textContent=e.label,e.readonly&&n.classList.add("pf-tab-readonly"),"markdown"===e.type&&n.classList.add("pf-tab-markdown"),n.addEventListener("click",()=>te(e)),j.appendChild(n)}),n.length>0&&te(n[0],!0)}function te(e,n){if(n||Z!==e)if(Z=e,j.querySelectorAll(".pf-tab").forEach(n=>{n.classList.toggle("pf-tab-active",n.dataset.tabId===e.id)}),"markdown"===e.type){if(document.getElementById("pf-ace").style.display="none",L.style.display="block",window.marked){let n=marked.parse(e.starterCode);window.mermaid&&(n=n.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,(e,n)=>`<div class="mermaid">${n.replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">")}</div>`)),L.innerHTML=`<div class="pf-md-inner">${n}</div>`}else L.innerHTML=`<div class="pf-md-inner"><pre>${e.starterCode}</pre></div>`;window.mermaid&&mermaid.run({nodes:L.querySelectorAll(".mermaid")})}else document.getElementById("pf-ace").style.display="block",L.style.display="none",q&&Q[e.id]&&(q.setSession(Q[e.id]),q.setReadOnly(e.readonly),q.focus())}function ae(){let n=1;e.filter(e=>"python"===e.type).forEach(e=>{e.hidden||e.readonly||!Q[e.id]?n+=e.code.split("\n").length:(Q[e.id].setOption("firstLineNumber",n),n+=Q[e.id].getLength())})}function re(){Object.keys(Q).forEach(e=>delete Q[e]),e.filter(e=>!e.hidden&&"python"===e.type).forEach(e=>{const n=ace.createEditSession(e.code,"ace/mode/python");if(n.setUseWorker(!1),n.setTabSize(4),Q[e.id]=n,!e.readonly){let e=null;n.on("change",()=>{null!==e&&(clearTimeout(e),ee.delete(e)),e=setTimeout(()=>{ee.delete(e),e=null,ie()},350),ee.add(e),ae(),de()})}});const n=e.find(e=>!e.hidden&&"python"===e.type);q&&n&&Q[n.id]&&(q.setSession(Q[n.id]),q.setReadOnly(n.readonly),q.renderer.updateFull(!0)),ae()}function oe(){!r.ace.startsWith("vendor")&&r.ace.startsWith("http")||ace.config.set("basePath",r.ace.replace(/\/[^/]+$/,"/")),q=ace.edit("pf-ace"),q.setTheme("ace/theme/monokai"),q.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),q.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{q.completer?.popup?.isOpen||Ae()}}),q.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:A}),q.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:se}),q.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}}),re(),ne(),de()}function ie(){const n={v:1,tabs:e.map(e=>({label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,content:e.hidden||e.readonly||"python"!==e.type||!Q[e.id]?e.code:Q[e.id].getValue()}))};try{localStorage.setItem(a,JSON.stringify(n))}catch(e){}}function se(){ie()}function de(){const n=s||e.some(e=>!e.hidden&&!e.readonly&&"python"===e.type&&Q[e.id]&&Q[e.id].getValue()!==e.starterCode);k.classList.toggle("pf-dirty",n)}function le(){ee.forEach(e=>clearTimeout(e)),ee.clear();try{localStorage.removeItem(a)}catch(e){}e.forEach(e=>{if(e.label)try{localStorage.removeItem(a+":"+e.label.replace(/[^a-zA-Z0-9]/g,"_"))}catch(e){}});try{localStorage.removeItem(a+":Code")}catch(e){}s=!1,e=t.map((e,n)=>({...e,id:"tab-"+n,code:e.starterCode})),re(),ne(),de(),Ae()}window.addEventListener("beforeunload",se);let ce=null,pe=null;async function me(){return pe||(pe=(async()=>{const e={};if(r.pyodideIndex&&(e.indexURL=r.pyodideIndex),ce=await loadPyodide(e),await ce.loadPackage(["rich","pygments"]),ce.runPython("\nimport sys, types, js\nfrom js import p5py, _pfMouse\nfrom pyodide.ffi import JsProxy\n\n# ── Python builtins that must NOT be shadowed ──────────────────────\n_BLACKLIST = frozenset({\n 'abs','all','any','bin','bool','bytes','callable','chr','compile',\n 'delattr','dict','dir','divmod','enumerate','eval','exec',\n 'filter','float','format','frozenset','getattr','globals','hasattr',\n 'hash','help','hex','id','input','int','isinstance','issubclass',\n 'iter','len','list','locals','map','max','min','next','object',\n 'oct','open','ord','pow','print','property','range','repr',\n 'reversed','round','set','setattr','slice','sorted','staticmethod',\n 'str','sum','super','tuple','type','vars','zip',\n # p5 lifecycle hooks — user defines these, we don't import them\n 'setup','draw','preload',\n})\n\n# ── Introspect a hidden dummy p5 instance ─────────────────────────\n_dummy_node = js.document.createElement('div')\n_dummy = js.p5.new(lambda _: None, _dummy_node)\n\n_p5_functions = set() # names of callable JS members\n_p5_attributes = set() # names of scalar/readable members\n\nfor _n in dir(_dummy):\n if _n.startswith('_') or _n in _BLACKLIST:\n continue\n _v = getattr(_dummy, _n)\n if isinstance(_v, JsProxy):\n if callable(_v):\n _p5_functions.add(_n)\n # non-callable JsProxy (canvas, pixels…) → skip\n else:\n _p5_attributes.add(_n)\n\n# Read real initial values now, while dummy is still alive\n_attr_init = {}\nfor _n in _p5_attributes:\n try:\n _attr_init[_n] = getattr(_dummy, _n)\n except Exception:\n _attr_init[_n] = 0\n\n_dummy.remove()\ndel _dummy, _dummy_node\n\n# ── Build module ───────────────────────────────────────────────────\nm = types.ModuleType(\"p5\")\n\n# Generic function wrapper: delegates to live p5Bridge instance\nclass _FW:\n __slots__ = ('_n',)\n def __init__(self, n): self._n = n\n def __call__(self, *a): return getattr(p5py, self._n)(*a)\n def __repr__(self): return f'<p5 function {self._n}>'\n\nfor _n in _p5_functions:\n setattr(m, _n, _FW(_n))\n\n# ── Special overrides (our bridge has custom behaviour) ────────────\n# smooth/noSmooth exist on a real p5 instance so introspection finds\n# them — but our Proxy overrides them to also toggle CSS image-rendering.\n# size and sketchTitle are pyfrilet-only: NOT on a real p5 instance,\n# so introspection misses them — add them explicitly.\nfor _n in ('sketchTitle',):\n setattr(m, _n, _FW(_n))\n _p5_functions.add(_n) # keep __all__ consistent\n\n# size() calls _pf_refresh after resizing so width/height are immediately\n# correct in setup() — consistent with p5.js JS behaviour.\nclass _SizeWrapper:\n def __call__(self, *a):\n p5py.size(*a)\n _pf_refresh(_ns_ref[0])\n return _GetCanvasWrapper()()\n def __repr__(self): return '<p5 function size>'\nsetattr(m, 'size', _SizeWrapper())\nsetattr(m, 'createCanvas', m.size) # alias — createCanvas(...) == size(...)\n_p5_functions.add('size')\n_p5_functions.add('createCanvas')\n_ns_ref = [{}] # filled in by runCode before each exec\n\n# getCanvas() — returns the p5.Element wrapping the canvas,\n# so the user can call .drop(create_proxy(fn)), .mouseOver(), etc. directly like in JS.\nclass _GetCanvasWrapper:\n def __call__(self):\n p = p5py._p\n if p is None:\n raise RuntimeError('getCanvas() doit être appelé dans setup() ou après')\n p.canvas.id = '__pf_canvas__'\n return p.select('#__pf_canvas__')\n def __repr__(self): return '<p5 function getCanvas>'\nsetattr(m, 'getCanvas', _GetCanvasWrapper())\n_p5_functions.add('getCanvas')\n\n# mouseX / mouseY: override with our accurate coordinate calculator\n# (p5's own values are wrong when a CSS-transformed parent is used)\n_MOUSE_OVERRIDE = frozenset({'mouseX', 'mouseY'})\n\n# Initial values from the dummy instance — constants like WEBGL, DEGREES,\n# LEFT_ARROW… are correct from the very first setup() call.\nfor _n in _p5_attributes:\n if _n in _MOUSE_OVERRIDE:\n setattr(m, _n, 0.0)\n else:\n setattr(m, _n, _attr_init.get(_n, 0))\n\n# Build __all__ for import * — done later, after snake_case aliases are added\n\n# ── _pf_refresh: called before every event callback ───────────────\nimport re as _re\n\n# Pre-compute snake_case alias for each attribute — None if identical\n_attr_snake = {\n _k: (_re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _k) or None)\n for _k in _p5_attributes\n}\n_attr_snake = {_k: (_s if _s != _k else None) for _k, _s in _attr_snake.items()}\n\n# Add snake_case names to _p5_attributes so __all__ and _pf_refresh cover them\nfor _k, _sk in list(_attr_snake.items()):\n if _sk:\n _p5_attributes.add(_sk)\n setattr(m, _sk, getattr(m, _k, 0)) # initial value mirrors camelCase\n _attr_snake[_sk] = None # snake name has no further alias\n\ndef _pf_refresh(ns):\n # accurate mouse coords (bypasses p5's stale CSS-transform offset)\n mx, my = _pfMouse()\n\n # update all known scalar attributes from live instance\n for _k in _p5_attributes:\n _sk = _attr_snake.get(_k)\n if _k in _MOUSE_OVERRIDE:\n _v = mx if _k in ('mouseX', 'mouse_x') else my\n elif _sk is None and _k not in _attr_snake:\n # pure snake_case entry — skip, updated via its camelCase counterpart\n continue\n else:\n try:\n _v = getattr(p5py, _k)\n except Exception:\n continue\n setattr(m, _k, _v)\n if _k in ns:\n ns[_k] = _v\n if _sk:\n setattr(m, _sk, _v)\n if _sk in ns:\n ns[_sk] = _v\n\nsys.modules[\"p5\"] = m\n\n# ── draw() watchdog via sys.settrace ──────────────────────────────\n# Trace is called on every Python line event. We only call time.monotonic()\n# every N events to minimize overhead — a tight loop still triggers within\n# a few microseconds, so detection latency is negligible.\nimport time as _time\n\n_WDOG_CHECK_EVERY = 100\n_wdog_deadline = [0.0]\n_wdog_count = [0]\n\ndef _wdog_trace(frame, event, arg):\n _wdog_count[0] += 1\n if _wdog_count[0] >= _WDOG_CHECK_EVERY:\n _wdog_count[0] = 0\n if _time.monotonic() > _wdog_deadline[0]:\n raise TimeoutError(\"draw() watchdog\")\n return _wdog_trace\n\nfrom rich.console import Console as _RichConsole\n_pf_rich_console = _RichConsole(stderr=True)\n\nclass _PfHandledError(Exception):\n \"\"\"Levée après que rich a déjà affiché le traceback vers xterm.\"\"\"\n pass\n\ndef _pf_safe_call(fn):\n try:\n fn()\n except (_PfHandledError, TimeoutError):\n raise\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename not in ('<string>', '<pyfrilet>'):\n _tb = _tb.tb_next\n if _tb: _e.__traceback__ = _tb\n _pf_rich_console.print_exception(show_locals=False)\n from js import _pfShowErrorTerminal\n _pfShowErrorTerminal()\n\ndef _pf_safe_proxy(fn):\n from pyodide.ffi import create_proxy as _cp\n def _wrapped(*args, **kwargs):\n _pf_safe_call(lambda: fn(*args, **kwargs))\n return _cp(_wrapped)\n\nsetattr(m, 'safe_proxy', _pf_safe_proxy)\n_p5_functions.add('safe_proxy')\n\ndef _pf_draw_watchdog(fn, timeout_ms):\n _wdog_count[0] = 0\n _wdog_deadline[0] = _time.monotonic() + timeout_ms * 0.001\n sys.settrace(_wdog_trace)\n try:\n _pf_safe_call(fn)\n finally:\n sys.settrace(None)\n\ndef _pf_draw_direct(fn, timeout_ms):\n _pf_safe_call(fn)\n\ndef _snake_to_camel(name):\n parts = name.split('_')\n return parts[0] + ''.join(p.capitalize() for p in parts[1:])\n\n# Pre-populate snake_case aliases so \"from p5 import no_fill\" works\nfor _camel in list(vars(m).keys()):\n _snake = _re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _camel)\n if _snake != _camel and not hasattr(m, _snake):\n setattr(m, _snake, getattr(m, _camel))\n if _camel in _p5_functions:\n _p5_functions.add(_snake)\n\n# Rebuild __all__ now that snake_case aliases are included\nm.__all__ = sorted(_p5_functions | _p5_attributes)\n\ndef _p5_getattr(name):\n camel = _snake_to_camel(name)\n if camel != name:\n val = getattr(m, camel, None)\n if val is not None:\n return val\n raise AttributeError(f\"module 'p5' has no attribute '{name}'\")\n\nm.__getattr__ = _p5_getattr\n"),ce.runPython("\nimport asyncio as _asyncio, ast as _ast\nimport os as _os, sys as _sys\n_os.environ.setdefault('TERM', 'xterm-256color')\n_os.environ.setdefault('COLORTERM', 'truecolor')\n\n# Wrapper file-like qui écrit directement vers xterm via JS.\n# Rich écrit des strings sur sys.stdout.write() — il faut un vrai objet fichier.\nclass _PfStream:\n def __init__(self, js_fn):\n self._fn = js_fn\n self.encoding = 'utf-8'\n self.errors = 'replace'\n def write(self, s):\n if s:\n self._fn(s)\n return len(s)\n def writelines(self, lines):\n for l in lines: self.write(l)\n def flush(self): pass\n def isatty(self): return True\n @property\n def softspace(self): return 0\n\nfrom js import _pfTermWrite, _pfTermWriteErr\n_sys.stdout = _PfStream(_pfTermWrite)\n_sys.stderr = _PfStream(_pfTermWriteErr)\n\nasync def _pf_async_input(prompt=\"\"):\n from js import _pfTerminalInput\n result = await _pfTerminalInput(str(prompt) if prompt else \"\")\n return result\n\nasync def _pf_run_terminal(source):\n class _InputAwaiter(_ast.NodeTransformer):\n def visit_Call(self, node):\n self.generic_visit(node)\n if isinstance(node.func, _ast.Name) and node.func.id == 'input':\n return _ast.Await(value=node)\n return node\n\n tree = _ast.parse(source)\n tree = _InputAwaiter().visit(tree)\n\n wrapper = _ast.parse(\"async def programme(): pass\")\n wrapper.body[0].body = tree.body if tree.body else [_ast.Pass()]\n _ast.fix_missing_locations(wrapper)\n\n _ns = {'input': _pf_async_input}\n exec(compile(wrapper, '<pyfrilet>', 'exec'), _ns)\n try:\n await _ns['programme']()\n except SystemExit:\n pass\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename != '<pyfrilet>':\n _tb = _tb.tb_next\n if _tb:\n _e.__traceback__ = _tb\n _pf_rich_console.print_exception(show_locals=False)\n"),q){fe(ce.runPython("list(m.__all__)").toJs())}})(),pe)}function fe(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,a,r,o){o(null,r.length>0?n:[])}},a=ace.require("ace/ext/language_tools");a&&Array.isArray(a.completers)&&(a.completers=a.completers.filter(e=>!0!==e._pyfrilet)),t._pyfrilet=!0,q.completers=[...q.completers||[],t]}let ue=!1,_e=!1,he=null,ye=null,be=null,ge=null,we=null,ve=null,xe=null,ke=null,Ee=null,Ce=null,Se=null,je=null,Le=null,ze=null;const Re=300;function Ie(e){return!/\bfrom\s+p5\s+import\b|\bimport\s+p5\b/.test(e)}async function Te(e){_e=!0,qe(),Xe(),H();try{const n=ce.globals.get("_pf_run_terminal");await n(e)}catch(e){const n=String(e);n.includes("SystemExit")||Ve(n+"\n")}finally{_e=!1}}async function Ae(){if(ue){if(!_e)return;Qe(),ue=!1,y.classList.remove("pf-running"),await new Promise(e=>setTimeout(e,30))}ue=!0,y.classList.add("pf-running"),H(),Xe(),X(),ce||(_.textContent="Initialisation de Pyodide…",u.style.display="flex");try{await me()}catch(e){return u.style.display="none",$("Erreur Pyodide : "+(e.message||String(e))),ue=!1,void y.classList.remove("pf-running")}u.style.display="none";const t=e.filter(e=>"python"===e.type).map(e=>e.hidden||e.readonly||!Q[e.id]?e.code:Q[e.id].getValue()).join("\n");try{_.textContent="Chargement des dépendances…",u.style.display="flex",await ce.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:n})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}if(u.style.display="none",Ie(t))return y.classList.remove("pf-running"),await Te(t),void(ue=!1);Qe(),ce.globals.set("_USER_CODE",t);try{ce.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)"),ce.runPython("_ns_ref[0] = _ns")}catch(e){return Ze(e.message||String(e)),ue=!1,void y.classList.remove("pf-running")}let a,r,i,s,d,l,c,p,f,h,b,g,w,v;try{const e=(e,n)=>ce.runPython(`_ns.get('${e}') or _ns.get('${n}')`);d=e("preload","preload"),a=e("setup","setup"),r=e("draw","draw"),i=e("mousePressed","mouse_pressed"),s=e("keyPressed","key_pressed"),l=e("mouseDragged","mouse_dragged"),c=e("mouseReleased","mouse_released"),p=e("mouseMoved","mouse_moved"),f=e("mouseWheel","mouse_wheel"),h=e("doubleClicked","double_clicked"),b=e("keyReleased","key_released"),g=e("touchStarted","touch_started"),w=e("touchMoved","touch_moved"),v=e("touchEnded","touch_ended")}catch(e){return Ze(e.message||String(e)),ue=!1,void y.classList.remove("pf-running")}if(!r)return $("Le script doit définir au moins une fonction draw()."),ue=!1,void y.classList.remove("pf-running");const{create_proxy:x}=ce.pyimport("pyodide.ffi"),k=ce.runPython("_ns.get('windowResized')"),E=ce.globals.get("_pf_refresh"),C=ce.globals.get(o?"_pf_draw_direct":"_pf_draw_watchdog"),S=ce.globals.get("_ns"),j=ce.globals.get("_pf_safe_call"),L=e=>e?x(()=>{try{E(S),j(e)}catch(e){Ze("")}}):null;be=d?x(()=>{try{j(d)}catch(e){Ze("")}}):null,he=a?x(()=>{try{j(a)}catch(e){Ze("")}}):null,ye=x(()=>{try{E(S),C(r,Re)}catch(e){const n=String(e);X(),n.includes("TimeoutError")||n.includes("watchdog")?$(`draw() a dépassé ${Re}ms — sketch arrêté (watchdog).`):Ze("")}}),ge=L(i),we=L(c),ve=L(l),xe=L(p),ke=L(f),Ee=L(h),Ce=L(s),Se=L(b),je=L(g),Le=L(w),ze=L(v);const z=k?x(()=>{try{j(k)}catch(e){Ze("")}}):null;let R=!1;J=new p5(e=>{V._setP(e),be&&(e.preload=()=>{be()}),e.setup=()=>{he&&he(),e.canvas||V.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),R=!0},e.draw=()=>{R&&ye()},e.mousePressed=()=>{R&&ge&&ge()},e.mouseReleased=()=>{R&&we&&we()},e.mouseDragged=()=>{R&&ve&&ve()},e.mouseMoved=()=>{R&&xe&&xe()},e.mouseWheel=e=>{R&&ke&&ke()},e.doubleClicked=()=>{R&&Ee&&Ee()},e.keyPressed=()=>{R&&Ce&&Ce()},e.keyReleased=()=>{R&&Se&&Se()},je&&(e.touchStarted=()=>{R&&je()}),Le&&(e.touchMoved=()=>{R&&Le()}),ze&&(e.touchEnded=()=>{R&&ze()}),e.windowResized=()=>{"fullscreen"===V._mode?V.size("max"):Y(),z&&z()}},m),ue=!1,y.classList.remove("pf-running")}const Pe='<!doctype html>\n<html lang="fr">\n<head>\n <meta charset="utf-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <title>export</title>\n <script src="https://cdn.jsdelivr.net/npm/pyfrilet@0.6.0/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\nFILLME-SCRIPTS\n\n</body>\n</html>';function Me(){const n=e.map((e,n)=>{let t;t="python"!==e.type||e.hidden||e.readonly||!Q[e.id]?e.code:Q[e.id].getValue();const a=[],r="markdown"===e.type?"text/markdown":"text/python";null!==e.label&&a.push(`data-tab="${e.label.replace(/"/g,"&quot;")}"`),e.hidden&&a.push("data-hidden"),e.readonly&&a.push("data-readonly");return`<script type="${r}"${a.length?" "+a.join(" "):""}>\n${t.replace(/<\/script>/gi,"<\\/script>")}\n<\/script>`}).join("\n\n"),t=Pe.replace("FILLME-SCRIPTS",n),a=new Blob([t],{type:"text/html;charset=utf-8"}),r=URL.createObjectURL(a),o=Object.assign(document.createElement("a"),{href:r,download:"sketch.html"});document.body.appendChild(o),o.click(),document.body.removeChild(o),URL.revokeObjectURL(r)}let Be=null,Oe=[];function We(){const e=V._p?.canvas;if(!e)return;const n=["video/webm;codecs=vp9","video/webm;codecs=vp8","video/webm"].find(e=>MediaRecorder.isTypeSupported(e))||"video/webm",t=e.captureStream();Be=new MediaRecorder(t,{mimeType:n}),Oe=[],Be.ondataavailable=e=>{e.data.size&&Oe.push(e.data)},Be.onstop=()=>{const e=new Blob(Oe,{type:n}),t=URL.createObjectURL(e),a=n.includes("webm")?"webm":"mp4";Object.assign(document.createElement("a"),{href:t,download:`sketch.${a}`}).click(),URL.revokeObjectURL(t),x.textContent="⏺",x.title="Enregistrer WebM",x.classList.remove("pf-recording"),Be=null},Be.start(),x.textContent="⏹",x.title="Arrêter l'enregistrement",x.classList.add("pf-recording")}function Fe(){Be&&"inactive"!==Be.state&&Be.stop()}x.addEventListener("click",()=>{Be?Fe():We()}),y.addEventListener("click",()=>Ae()),w.addEventListener("click",()=>{z?A():(R=window.innerHeight-32,I(),T())}),v.addEventListener("click",Me);const De="https://codeberg.org/nopid/pyfrilet";function Ne(e){return new Promise((n,t)=>{const a=document.createElement("script");a.src=e,a.onload=n,a.onerror=()=>t(new Error("Impossible de charger : "+e)),document.head.appendChild(a)})}E.addEventListener("click",()=>window.open(De,"_blank")),k.addEventListener("click",()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}),window.addEventListener("keydown",e=>{const n=z&&q&&q.isFocused&&q.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void Ae();if("Escape"===e.key){const t=document.querySelector(".ace_search");if(t&&"none"!==t.style.display)return e.preventDefault(),e.stopPropagation(),q.searchBox?q.searchBox.hide():t.style.display="none",void q.focus();if(n){const n=q.completer?.popup?.isOpen;if(n)return;return e.preventDefault(),e.stopPropagation(),void A()}return e.preventDefault(),e.stopPropagation(),void(z?A():T())}if(!n)return"s"!==e.key&&"S"!==e.key||!e.ctrlKey&&!e.metaKey?"r"!==e.key&&"R"!==e.key||!e.ctrlKey&&!e.metaKey||e.altKey?void 0:(e.preventDefault(),void(confirm("Réinitialiser ? Les modifications seront perdues.")&&le())):(e.preventDefault(),void se())}else e.preventDefault()},!0),(async()=>{_.textContent="Chargement des dépendances…",u.style.display="flex";try{if(await Ne(r.p5),r.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=r.katexCss,document.head.appendChild(e),await Ne(r.marked),await Ne(r.katex),await Ne(r.markedKatex),await Ne(r.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"neutral"})}await Ne(r.ace),await Ne(r.acePython),await Ne(r.aceMonokai),await Ne(r.aceLangTools),await Ne(r.aceSearchbox),await Ne(r.pyodide);const e=document.createElement("link");e.rel="stylesheet",e.href=r.xtermCss,document.head.appendChild(e),await Ne(r.xterm),await Ne(r.xtermFit),await Ne(r.xtermUni)}catch(e){return _.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}oe(),await Ae(),u.style.display="none"})();const Ue=document.getElementById("pf-xterm");let Ke=null,$e=null,He=null,Ye="";function Ge(){if(Ke)return;Ke=new Terminal({theme:{background:"#000000",foreground:"#e8e8e8",cursor:"#ffffff",black:"#2a2a2a",brightBlack:"#555555",red:"#cc4444",brightRed:"#ff6666",green:"#44aa44",brightGreen:"#66cc66",yellow:"#aaaa00",brightYellow:"#dddd44",blue:"#4466cc",brightBlue:"#6688ff",magenta:"#aa44aa",brightMagenta:"#dd66dd",cyan:"#44aaaa",brightCyan:"#66cccc",white:"#cccccc",brightWhite:"#ffffff"},fontFamily:"'Fira Code', 'Consolas', 'Courier New', monospace",fontSize:15,lineHeight:1,letterSpacing:0,cursorBlink:!0,scrollback:2e3,convertEol:!0,allowProposedApi:!0}),$e=new FitAddon.FitAddon,Ke.loadAddon($e);const e=new Unicode11Addon.Unicode11Addon;Ke.loadAddon(e),Ke.unicode.activeVersion="11",Ke.open(Ue),$e.fit(),new ResizeObserver(()=>{Ke&&"none"!==Ue.style.display&&$e.fit()}).observe(Ue),Ke.onData(e=>{if(He)if("\r"===e){const e=Ye;Ye="",Ke.write("\r\n");const n=He;He=null,n(e)}else if(""===e)Ye.length>0&&(Ye=Ye.slice(0,-1),Ke.write("\b \b"));else if(""===e){Ye="",Ke.write("^C\r\n");const e=He;He=null,e(null)}else e.charCodeAt(0)>=32&&(Ye+=e,Ke.write(e))})}function Je(e){Ke&&Ke.write(e)}function Ve(e){Ge(),Ke.write(""),Ke.write(e.replace(/\n/g,"\r\n")),Ke.write("")}function Xe(){Ke&&Ke.clear(),He=null,Ye=""}function qe(){f.style.display="none",Ue.style.display="block",Ue.classList.remove("pf-xterm-overlay"),Ge(),$e.fit(),Ke.focus()}function Ze(e){Ue.style.display="block",Ue.classList.add("pf-xterm-overlay"),Ge(),$e.fit(),e&&(Xe(),Ke.write("── Erreur ──────────────────────────────────────\r\n\r\n"),Ke.write(e.replace(/\n/g,"\r\n")+"\r\n"))}function Qe(){if(Ue.style.display="none",Ue.classList.remove("pf-xterm-overlay"),f.style.display="",He){const e=He;He=null,Ye="",e("")}}window._pfTerminalInput=function(e){return new Promise(n=>{He=n,Ye="",e&&Ke.write(e),Ke.focus()})},window._pfTermWrite=Je,window._pfTermWriteErr=Ve,window._pfShowErrorTerminal=()=>{X(),Ze("")}}(z,j,S,C,A,T)})}();