fastled 1.2.33__py3-none-any.whl → 1.4.50__py3-none-any.whl

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.
Files changed (58) hide show
  1. fastled/__init__.py +51 -192
  2. fastled/__main__.py +14 -0
  3. fastled/__version__.py +6 -0
  4. fastled/app.py +124 -27
  5. fastled/args.py +124 -0
  6. fastled/assets/localhost-key.pem +28 -0
  7. fastled/assets/localhost.pem +27 -0
  8. fastled/cli.py +10 -2
  9. fastled/cli_test.py +21 -0
  10. fastled/cli_test_interactive.py +21 -0
  11. fastled/client_server.py +334 -55
  12. fastled/compile_server.py +12 -1
  13. fastled/compile_server_impl.py +115 -42
  14. fastled/docker_manager.py +392 -69
  15. fastled/emoji_util.py +27 -0
  16. fastled/filewatcher.py +100 -8
  17. fastled/find_good_connection.py +105 -0
  18. fastled/header_dump.py +63 -0
  19. fastled/install/__init__.py +1 -0
  20. fastled/install/examples_manager.py +62 -0
  21. fastled/install/extension_manager.py +113 -0
  22. fastled/install/main.py +156 -0
  23. fastled/install/project_detection.py +167 -0
  24. fastled/install/test_install.py +373 -0
  25. fastled/install/vscode_config.py +344 -0
  26. fastled/interruptible_http.py +148 -0
  27. fastled/keyboard.py +1 -0
  28. fastled/keyz.py +84 -0
  29. fastled/live_client.py +26 -1
  30. fastled/open_browser.py +133 -89
  31. fastled/parse_args.py +219 -15
  32. fastled/playwright/chrome_extension_downloader.py +207 -0
  33. fastled/playwright/playwright_browser.py +773 -0
  34. fastled/playwright/resize_tracking.py +127 -0
  35. fastled/print_filter.py +52 -0
  36. fastled/project_init.py +20 -13
  37. fastled/select_sketch_directory.py +142 -17
  38. fastled/server_flask.py +487 -0
  39. fastled/server_start.py +21 -0
  40. fastled/settings.py +53 -4
  41. fastled/site/build.py +2 -10
  42. fastled/site/examples.py +10 -0
  43. fastled/sketch.py +129 -7
  44. fastled/string_diff.py +218 -9
  45. fastled/test/examples.py +7 -5
  46. fastled/types.py +22 -2
  47. fastled/util.py +78 -0
  48. fastled/version.py +41 -0
  49. fastled/web_compile.py +401 -218
  50. fastled/zip_files.py +76 -0
  51. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/METADATA +533 -382
  52. fastled-1.4.50.dist-info/RECORD +60 -0
  53. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/WHEEL +1 -1
  54. fastled/open_browser2.py +0 -111
  55. fastled-1.2.33.dist-info/RECORD +0 -33
  56. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/entry_points.txt +0 -0
  57. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info/licenses}/LICENSE +0 -0
  58. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,773 @@
1
+ """
2
+ Playwright browser integration for FastLED WASM compiler.
3
+
4
+ This module provides a Playwright-based browser implementation that can be used
5
+ in a Playwright browser instead of the default system browser when
6
+ Playwright is available.
7
+ """
8
+
9
+ import asyncio
10
+ import os
11
+ import sys
12
+ import threading
13
+ import warnings
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from fastled.playwright.resize_tracking import ResizeTracker
18
+
19
+ # Set custom Playwright browser installation path
20
+ PLAYWRIGHT_DIR = Path.home() / ".fastled" / "playwright"
21
+ PLAYWRIGHT_DIR.mkdir(parents=True, exist_ok=True)
22
+
23
+
24
+ def get_chromium_executable_path() -> str | None:
25
+ """Get the path to the custom Chromium executable if it exists."""
26
+ import glob
27
+ import platform
28
+
29
+ playwright_dir = PLAYWRIGHT_DIR
30
+
31
+ if platform.system() == "Windows":
32
+ chromium_pattern = str(
33
+ playwright_dir / "chromium-*" / "chrome-win" / "chrome.exe"
34
+ )
35
+ elif platform.system() == "Darwin": # macOS
36
+ chromium_pattern = str(
37
+ playwright_dir
38
+ / "chromium-*"
39
+ / "chrome-mac"
40
+ / "Chromium.app"
41
+ / "Contents"
42
+ / "MacOS"
43
+ / "Chromium"
44
+ )
45
+ else: # Linux
46
+ chromium_pattern = str(
47
+ playwright_dir / "chromium-*" / "chrome-linux" / "chrome"
48
+ )
49
+
50
+ matches = glob.glob(chromium_pattern)
51
+ return matches[0] if matches else None
52
+
53
+
54
+ class PlaywrightBrowser:
55
+ """Playwright browser manager for FastLED sketches."""
56
+
57
+ def __init__(self, headless: bool = False, enable_extensions: bool = True):
58
+ """Initialize the Playwright browser manager.
59
+
60
+ Args:
61
+ headless: Whether to run the browser in headless mode
62
+ enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
63
+ """
64
+
65
+ self.headless = headless
66
+ self.enable_extensions = enable_extensions
67
+ self.auto_resize = True # Always enable auto-resize
68
+ self.browser: Any = None
69
+ self.context: Any = None
70
+ self.page: Any = None
71
+ self.playwright: Any = None
72
+ self._should_exit = asyncio.Event()
73
+ self._extensions_dir: Path | None = None
74
+ self.resize_tracker: ResizeTracker | None = None
75
+
76
+ # Initialize extensions if enabled
77
+ if self.enable_extensions:
78
+ self._setup_extensions()
79
+
80
+ def _setup_extensions(self) -> None:
81
+ """Setup Chrome extensions for enhanced debugging."""
82
+ try:
83
+ from fastled.playwright.chrome_extension_downloader import (
84
+ download_cpp_devtools_extension,
85
+ )
86
+
87
+ extension_path = download_cpp_devtools_extension()
88
+ if extension_path and extension_path.exists():
89
+ self._extensions_dir = extension_path
90
+ print(
91
+ f"[PYTHON] C++ DevTools Support extension ready: {extension_path}"
92
+ )
93
+ else:
94
+ print("[PYTHON] Warning: C++ DevTools Support extension not available")
95
+ self.enable_extensions = False
96
+ except Exception as e:
97
+ print(f"[PYTHON] Warning: Failed to setup Chrome extensions: {e}")
98
+ self.enable_extensions = False
99
+
100
+ def _detect_device_scale_factor(self) -> float | None:
101
+ """Detect the system's device scale factor for natural browser behavior.
102
+
103
+ Returns:
104
+ The detected device scale factor, or None if detection fails or
105
+ the value is outside reasonable bounds (0.5-4.0).
106
+ """
107
+ try:
108
+ import tkinter
109
+
110
+ root = tkinter.Tk()
111
+ root.withdraw() # Hide the window
112
+ scale_factor = root.winfo_fpixels("1i") / 72.0
113
+ root.destroy()
114
+
115
+ # Validate the scale factor is in a reasonable range
116
+ if 0.5 <= scale_factor <= 4.0:
117
+ return scale_factor
118
+ else:
119
+ print(
120
+ f"[PYTHON] Detected scale factor {scale_factor:.2f} is outside reasonable bounds (0.5-4.0)"
121
+ )
122
+ return None
123
+
124
+ except Exception as e:
125
+ print(f"[PYTHON] Could not detect device scale factor: {e}")
126
+ return None
127
+
128
+ async def start(self) -> None:
129
+ """Start the Playwright browser."""
130
+ if self.browser is None and self.context is None:
131
+
132
+ from playwright.async_api import async_playwright
133
+
134
+ self.playwright = async_playwright()
135
+ playwright = await self.playwright.start()
136
+
137
+ if self.enable_extensions and self._extensions_dir:
138
+ # Use persistent context for extensions
139
+ user_data_dir = PLAYWRIGHT_DIR / "user-data"
140
+ user_data_dir.mkdir(parents=True, exist_ok=True)
141
+
142
+ launch_kwargs = {
143
+ "headless": False, # Extensions require headed mode
144
+ "channel": "chromium", # Required for extensions
145
+ "args": [
146
+ "--disable-dev-shm-usage",
147
+ "--disable-web-security",
148
+ "--allow-running-insecure-content",
149
+ f"--disable-extensions-except={self._extensions_dir}",
150
+ f"--load-extension={self._extensions_dir}",
151
+ ],
152
+ }
153
+
154
+ # Get custom Chromium executable path if available
155
+ executable_path = get_chromium_executable_path()
156
+ if executable_path:
157
+ launch_kwargs["executable_path"] = executable_path
158
+ print(
159
+ f"[PYTHON] Using custom Chromium executable: {executable_path}"
160
+ )
161
+
162
+ self.context = await playwright.chromium.launch_persistent_context(
163
+ str(user_data_dir), **launch_kwargs
164
+ )
165
+
166
+ print(
167
+ "[PYTHON] Started Playwright browser with C++ DevTools Support extension"
168
+ )
169
+
170
+ else:
171
+ # Regular browser launch without extensions
172
+ executable_path = get_chromium_executable_path()
173
+ launch_kwargs = {
174
+ "headless": self.headless,
175
+ "args": [
176
+ "--disable-dev-shm-usage",
177
+ "--disable-web-security",
178
+ "--allow-running-insecure-content",
179
+ ],
180
+ }
181
+
182
+ if executable_path:
183
+ launch_kwargs["executable_path"] = executable_path
184
+ print(
185
+ f"[PYTHON] Using custom Chromium executable: {executable_path}"
186
+ )
187
+
188
+ self.browser = await playwright.chromium.launch(**launch_kwargs)
189
+
190
+ if self.page is None:
191
+ if self.context:
192
+ # Using persistent context (with extensions)
193
+ if len(self.context.pages) > 0:
194
+ self.page = self.context.pages[0]
195
+ else:
196
+ self.page = await self.context.new_page()
197
+ elif self.browser:
198
+ # Using regular browser
199
+ # Detect system device scale factor for natural browser behavior
200
+ device_scale_factor = self._detect_device_scale_factor()
201
+
202
+ # Create browser context with detected or default device scale factor
203
+ if device_scale_factor:
204
+ context = await self.browser.new_context(
205
+ device_scale_factor=device_scale_factor
206
+ )
207
+ print(
208
+ f"[PYTHON] Created browser context with device scale factor: {device_scale_factor:.2f}"
209
+ )
210
+ else:
211
+ context = await self.browser.new_context()
212
+ print(
213
+ "[PYTHON] Created browser context with default device scale factor"
214
+ )
215
+
216
+ self.page = await context.new_page()
217
+
218
+ async def open_url(self, url: str) -> None:
219
+ """Open a URL in the Playwright browser.
220
+
221
+ Args:
222
+ url: The URL to open
223
+ """
224
+ if self.page is None:
225
+ await self.start()
226
+
227
+ print(f"Opening FastLED sketch in Playwright browser: {url}")
228
+ if self.page is not None:
229
+ await self.page.goto(url)
230
+
231
+ # Wait for the page to load
232
+ await self.page.wait_for_load_state("networkidle")
233
+
234
+ # Verify device scale factor is working correctly
235
+ try:
236
+ device_pixel_ratio = await self.page.evaluate("window.devicePixelRatio")
237
+ print(
238
+ f"[PYTHON] Verified browser device pixel ratio: {device_pixel_ratio}"
239
+ )
240
+ except Exception as e:
241
+ print(f"[PYTHON] Could not verify device pixel ratio: {e}")
242
+
243
+ # Set up auto-resizing functionality if enabled
244
+ if self.auto_resize:
245
+ await self._setup_auto_resize()
246
+
247
+ # Check if C++ DevTools extension is loaded
248
+ if self.enable_extensions and self._extensions_dir:
249
+ try:
250
+ # Check if the extension is available in the DevTools
251
+ extensions_available = await self.page.evaluate(
252
+ """
253
+ () => {
254
+ // Check if chrome.devtools is available (extension context)
255
+ return typeof chrome !== 'undefined' &&
256
+ typeof chrome.runtime !== 'undefined' &&
257
+ chrome.runtime.id !== undefined;
258
+ }
259
+ """
260
+ )
261
+
262
+ if extensions_available:
263
+ print(
264
+ "[PYTHON] ✅ C++ DevTools Support extension is active and ready for DWARF debugging"
265
+ )
266
+ else:
267
+ print(
268
+ "[PYTHON] ⚠️ C++ DevTools Support extension may not be fully loaded"
269
+ )
270
+
271
+ except Exception as e:
272
+ print(f"[PYTHON] Could not verify extension status: {e}")
273
+
274
+ async def _setup_auto_resize(self) -> None:
275
+ """Set up automatic window resizing based on content size."""
276
+ if self.page is None:
277
+ print("[PYTHON] Cannot setup auto-resize: page is None")
278
+ return
279
+
280
+ print(
281
+ "[PYTHON] Setting up browser window tracking with viewport-only adjustment"
282
+ )
283
+
284
+ # Create resize tracker instance
285
+ self.resize_tracker = ResizeTracker(self.page)
286
+
287
+ # Start polling loop that tracks browser window changes and adjusts viewport only
288
+ asyncio.create_task(self._track_browser_adjust_viewport())
289
+
290
+ async def _track_browser_adjust_viewport(self) -> None:
291
+ """Track browser window changes and adjust viewport accordingly.
292
+
293
+ This method polls for changes in the browser window size using the
294
+ ResizeTracker and handles any errors that occur.
295
+ """
296
+ if self.resize_tracker is None:
297
+ print("[PYTHON] Cannot start tracking: resize_tracker is None")
298
+ return
299
+
300
+ while not self._should_exit.is_set():
301
+ try:
302
+ # Wait between polls
303
+ await asyncio.sleep(0.25) # Poll every 250ms
304
+
305
+ # Check if page is still alive
306
+ if self.page is None or self.page.is_closed():
307
+ print("[PYTHON] Page closed, signaling exit")
308
+ self._should_exit.set()
309
+ return
310
+
311
+ # Update resize tracking
312
+ result = await self.resize_tracker.update()
313
+
314
+ if result is not None:
315
+ assert isinstance(result, Exception)
316
+ # An exception occurred in resize tracking
317
+ error_message = str(result)
318
+ warnings.warn(f"[PYTHON] Error in resize tracking: {error_message}")
319
+
320
+ # Be EXTREMELY conservative about browser close detection
321
+ # Only trigger shutdown on very specific errors that definitively indicate browser closure
322
+ browser_definitely_closed = any(
323
+ phrase in error_message.lower()
324
+ for phrase in [
325
+ "browser has been closed",
326
+ "target closed",
327
+ "connection closed",
328
+ "target page, probably because the page has been closed",
329
+ "page has been closed",
330
+ "browser context has been closed",
331
+ ]
332
+ )
333
+
334
+ # Also check actual browser state before deciding to shut down
335
+ browser_state_indicates_closed = False
336
+ try:
337
+ if self.browser and hasattr(self.browser, "is_closed"):
338
+ browser_state_indicates_closed = self.browser.is_closed()
339
+ elif self.context and hasattr(self.context, "closed"):
340
+ browser_state_indicates_closed = self.context.closed
341
+ except Exception:
342
+ # If we can't check the state, don't assume it's closed
343
+ warnings.warn(
344
+ f"[PYTHON] Could not check browser state: {result}. Assuming browser is not closed."
345
+ )
346
+ browser_state_indicates_closed = False
347
+
348
+ if browser_definitely_closed or browser_state_indicates_closed:
349
+ if browser_definitely_closed:
350
+ print(
351
+ f'[PYTHON] Browser has been closed because "{error_message}" matched one of the error phrases or browser state indicates closed, shutting down gracefully...'
352
+ )
353
+ elif browser_state_indicates_closed:
354
+ print(
355
+ "[PYTHON] Browser state indicates closed, shutting down gracefully..."
356
+ )
357
+ self._should_exit.set()
358
+ return
359
+ else:
360
+ # For other errors, just log and continue - don't shut down
361
+ print(
362
+ f"[PYTHON] Recoverable error in resize tracking: {result}"
363
+ )
364
+ # Add a small delay to prevent tight error loops
365
+ await asyncio.sleep(1.0)
366
+
367
+ except Exception as e:
368
+ error_message = str(e)
369
+ warnings.warn(
370
+ f"[PYTHON] Unexpected error in browser tracking loop: {error_message}"
371
+ )
372
+ # Add a small delay to prevent tight error loops
373
+ await asyncio.sleep(1.0)
374
+
375
+ warnings.warn("[PYTHON] Browser tracking loop exited.")
376
+
377
+ async def wait_for_close(self) -> None:
378
+ """Wait for the browser to be closed."""
379
+ if self.context:
380
+ # Wait for persistent context to be closed
381
+ try:
382
+ while not self.context.closed:
383
+ await asyncio.sleep(1)
384
+ except Exception:
385
+ pass
386
+ elif self.browser:
387
+ try:
388
+ # Wait for the browser to be closed
389
+ while not self.browser.is_closed():
390
+ await asyncio.sleep(1)
391
+ except Exception:
392
+ pass
393
+
394
+ async def close(self) -> None:
395
+ """Close the Playwright browser."""
396
+ # Signal all tracking loops to exit
397
+ self._should_exit.set()
398
+
399
+ try:
400
+ # Clean up resize tracker
401
+ self.resize_tracker = None
402
+
403
+ if self.page:
404
+ await self.page.close()
405
+ self.page = None
406
+
407
+ if self.context:
408
+ await self.context.close()
409
+ self.context = None
410
+
411
+ if self.browser:
412
+ await self.browser.close()
413
+ self.browser = None
414
+
415
+ if self.playwright:
416
+ # The playwright context manager may not have a stop() method in all versions
417
+ # Try stop() first, fall back to __aexit__ if needed
418
+ try:
419
+ if hasattr(self.playwright, "stop"):
420
+ await self.playwright.stop()
421
+ else:
422
+ # For async context managers, use __aexit__
423
+ await self.playwright.__aexit__(None, None, None)
424
+ except Exception as stop_error:
425
+ print(
426
+ f"[PYTHON] Warning: Could not properly stop playwright: {stop_error}"
427
+ )
428
+ # Try alternative cleanup methods
429
+ try:
430
+ if hasattr(self.playwright, "__aexit__"):
431
+ await self.playwright.__aexit__(None, None, None)
432
+ except Exception:
433
+ pass # Ignore secondary cleanup failures
434
+ finally:
435
+ self.playwright = None
436
+ except KeyboardInterrupt:
437
+ print("[PYTHON] Keyboard interrupt detected, closing Playwright browser")
438
+ self._should_exit.set()
439
+ import _thread
440
+
441
+ _thread.interrupt_main()
442
+ except Exception as e:
443
+ print(f"[PYTHON] Error closing Playwright browser: {e}")
444
+ self._should_exit.set()
445
+
446
+
447
+ def run_playwright_browser(
448
+ url: str, headless: bool = False, enable_extensions: bool = True
449
+ ) -> None:
450
+ """Run Playwright browser in a separate process.
451
+
452
+ Args:
453
+ url: The URL to open
454
+ headless: Whether to run in headless mode
455
+ enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
456
+ """
457
+
458
+ async def main():
459
+ browser = None
460
+ try:
461
+ browser = PlaywrightBrowser(
462
+ headless=headless, enable_extensions=enable_extensions
463
+ )
464
+ await browser.start()
465
+ await browser.open_url(url)
466
+
467
+ print("Playwright browser opened. Press Ctrl+C to close.")
468
+ await browser.wait_for_close()
469
+
470
+ except Exception as e:
471
+ # If we get an error that suggests browsers aren't installed, try to install them
472
+ if "executable doesn't exist" in str(e) or "Browser not found" in str(e):
473
+ print("🎭 Playwright browsers not found. Installing...")
474
+ if install_playwright_browsers():
475
+ print("🎭 Retrying browser startup...")
476
+ # Try again with fresh browser instance
477
+ browser = PlaywrightBrowser(
478
+ headless=headless, enable_extensions=enable_extensions
479
+ )
480
+ await browser.start()
481
+ await browser.open_url(url)
482
+
483
+ print("Playwright browser opened. Press Ctrl+C to close.")
484
+ await browser.wait_for_close()
485
+ else:
486
+ print("❌ Failed to install Playwright browsers")
487
+ raise e
488
+ else:
489
+ raise e
490
+ except KeyboardInterrupt:
491
+ print("\nClosing Playwright browser...")
492
+ finally:
493
+ if browser is not None:
494
+ try:
495
+ await browser.close()
496
+ except Exception as e:
497
+ print(f"Warning: Failed to close Playwright browser: {e}")
498
+
499
+ try:
500
+ asyncio.run(main())
501
+ except KeyboardInterrupt:
502
+ print("\nPlaywright browser closed.")
503
+ except Exception as e:
504
+ print(f"Playwright browser failed: {e}. Falling back to default browser.")
505
+ import webbrowser
506
+
507
+ webbrowser.open(url)
508
+
509
+
510
+ class PlaywrightBrowserProxy:
511
+ """Proxy object to manage Playwright browser lifecycle."""
512
+
513
+ def __init__(self):
514
+ self.process = None
515
+ self.browser_manager = None
516
+ self.monitor_thread = None
517
+ self._closing_intentionally = False
518
+
519
+ def open(
520
+ self, url: str, headless: bool = False, enable_extensions: bool = True
521
+ ) -> None:
522
+ """Open URL with Playwright browser and keep it alive.
523
+
524
+ Args:
525
+ url: The URL to open
526
+ headless: Whether to run in headless mode
527
+ enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
528
+ """
529
+
530
+ try:
531
+ # Run Playwright in a separate process to avoid blocking
532
+ import multiprocessing
533
+
534
+ self.process = multiprocessing.Process(
535
+ target=run_playwright_browser_persistent,
536
+ args=(url, headless, enable_extensions),
537
+ )
538
+ self.process.start()
539
+
540
+ # Start monitoring thread to exit main process when browser subprocess exits
541
+ self._start_monitor_thread()
542
+
543
+ # Register cleanup
544
+ import atexit
545
+
546
+ atexit.register(self.close)
547
+
548
+ except Exception as e:
549
+ warnings.warn(
550
+ f"Failed to start Playwright browser: {e}. Falling back to default browser."
551
+ )
552
+ import webbrowser
553
+
554
+ webbrowser.open(url)
555
+
556
+ def _start_monitor_thread(self) -> None:
557
+ """Start a thread to monitor the browser process and exit main process when it terminates."""
558
+ if self.monitor_thread is not None:
559
+ return
560
+
561
+ def monitor_process():
562
+ """Monitor the browser process and exit when it terminates."""
563
+ if self.process is None:
564
+ return
565
+
566
+ try:
567
+ # Wait for the process to terminate
568
+ self.process.join()
569
+
570
+ # Check if the process terminated (and we didn't kill it ourselves)
571
+ if (
572
+ self.process.exitcode is not None
573
+ and not self._closing_intentionally
574
+ ):
575
+ print("[MAIN] Browser closed, exiting main program")
576
+ # Force exit the entire program
577
+ os._exit(0)
578
+
579
+ except KeyboardInterrupt:
580
+ print("[MAIN] Browser monitor interrupted by user")
581
+ import _thread
582
+
583
+ _thread.interrupt_main()
584
+ except Exception as e:
585
+ print(f"[MAIN] Error monitoring browser process: {e}")
586
+
587
+ self.monitor_thread = threading.Thread(target=monitor_process, daemon=True)
588
+ self.monitor_thread.start()
589
+
590
+ def close(self) -> None:
591
+ """Close the Playwright browser."""
592
+ if self.process and self.process.is_alive():
593
+ print("Closing Playwright browser...")
594
+ # Mark that we're intentionally closing to prevent monitor from triggering exit
595
+ self._closing_intentionally = True
596
+ self.process.terminate()
597
+ self.process.join(timeout=5)
598
+ if self.process.is_alive():
599
+ self.process.kill()
600
+ self.process = None
601
+
602
+
603
+ def run_playwright_browser_persistent(
604
+ url: str, headless: bool = False, enable_extensions: bool = True
605
+ ) -> None:
606
+ """Run Playwright browser in a persistent mode that stays alive until terminated.
607
+
608
+ Args:
609
+ url: The URL to open
610
+ headless: Whether to run in headless mode
611
+ enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
612
+ """
613
+
614
+ async def main():
615
+ browser = None
616
+ try:
617
+ browser = PlaywrightBrowser(
618
+ headless=headless, enable_extensions=enable_extensions
619
+ )
620
+ await browser.start()
621
+ await browser.open_url(url)
622
+
623
+ print(
624
+ "Playwright browser opened. Browser will remain open until the FastLED process exits."
625
+ )
626
+
627
+ # Keep the browser alive until exit is signaled
628
+ while not browser._should_exit.is_set():
629
+ await asyncio.sleep(0.1)
630
+
631
+ except Exception as e:
632
+ # If we get an error that suggests browsers aren't installed, try to install them
633
+ if "executable doesn't exist" in str(e) or "Browser not found" in str(e):
634
+ print("🎭 Playwright browsers not found. Installing...")
635
+ if install_playwright_browsers():
636
+ print("🎭 Retrying browser startup...")
637
+ # Try again with fresh browser instance
638
+ browser = PlaywrightBrowser(
639
+ headless=headless, enable_extensions=enable_extensions
640
+ )
641
+ await browser.start()
642
+ await browser.open_url(url)
643
+
644
+ print(
645
+ "Playwright browser opened. Browser will remain open until the FastLED process exits."
646
+ )
647
+
648
+ # Keep the browser alive until exit is signaled
649
+ while not browser._should_exit.is_set():
650
+ await asyncio.sleep(0.1)
651
+ else:
652
+ print("❌ Failed to install Playwright browsers")
653
+ raise e
654
+ else:
655
+ raise e
656
+ except KeyboardInterrupt:
657
+ print("\nClosing Playwright browser...")
658
+ finally:
659
+ if browser is not None:
660
+ try:
661
+ await browser.close()
662
+ except Exception as e:
663
+ print(f"Warning: Failed to close Playwright browser: {e}")
664
+
665
+ try:
666
+ asyncio.run(main())
667
+ except KeyboardInterrupt:
668
+ print("\nPlaywright browser closed.")
669
+ except Exception as e:
670
+ print(f"Playwright browser failed: {e}")
671
+
672
+
673
+ def open_with_playwright(
674
+ url: str, headless: bool = False, enable_extensions: bool = True
675
+ ) -> PlaywrightBrowserProxy:
676
+ """Open URL with Playwright browser and return a proxy object for lifecycle management.
677
+
678
+ This function can be used as a drop-in replacement for webbrowser.open().
679
+
680
+ Args:
681
+ url: The URL to open
682
+ headless: Whether to run in headless mode
683
+ enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
684
+
685
+ Returns:
686
+ PlaywrightBrowserProxy object for managing the browser lifecycle
687
+ """
688
+ proxy = PlaywrightBrowserProxy()
689
+ proxy.open(url, headless, enable_extensions)
690
+ return proxy
691
+
692
+
693
+ def install_playwright_browsers() -> bool:
694
+ """Install Playwright browsers if not already installed.
695
+
696
+ Installs browsers to ~/.fastled/playwright directory.
697
+
698
+ Returns:
699
+ True if installation was successful or browsers were already installed
700
+ """
701
+
702
+ try:
703
+ import os
704
+ from pathlib import Path
705
+
706
+ # Set custom browser installation path
707
+ playwright_dir = Path.home() / ".fastled" / "playwright"
708
+ playwright_dir.mkdir(parents=True, exist_ok=True)
709
+
710
+ # Check if browsers are installed in the custom directory
711
+ import glob
712
+ import platform
713
+
714
+ if platform.system() == "Windows":
715
+ chromium_pattern = str(
716
+ playwright_dir / "chromium-*" / "chrome-win" / "chrome.exe"
717
+ )
718
+ elif platform.system() == "Darwin": # macOS
719
+ chromium_pattern = str(
720
+ playwright_dir / "chromium-*" / "chrome-mac" / "Chromium.app"
721
+ )
722
+ else: # Linux
723
+ chromium_pattern = str(
724
+ playwright_dir / "chromium-*" / "chrome-linux" / "chrome"
725
+ )
726
+
727
+ if glob.glob(chromium_pattern):
728
+ print(f"✅ Playwright browsers already installed at: {playwright_dir}")
729
+ return True
730
+
731
+ # If we get here, browsers need to be installed
732
+ print("Installing Playwright browsers...")
733
+ print(f"Installing to: {playwright_dir}")
734
+ import subprocess
735
+
736
+ env = dict(os.environ)
737
+ env["PLAYWRIGHT_BROWSERS_PATH"] = str(playwright_dir.resolve())
738
+
739
+ result = subprocess.run(
740
+ [sys.executable, "-m", "playwright", "install", "chromium"],
741
+ env=env,
742
+ )
743
+
744
+ if result.returncode == 0:
745
+ print("✅ Playwright browsers installed successfully!")
746
+ print(f" Location: {playwright_dir}")
747
+
748
+ # Also download the C++ DevTools Support extension
749
+ try:
750
+ from fastled.playwright.chrome_extension_downloader import (
751
+ download_cpp_devtools_extension,
752
+ )
753
+
754
+ extension_path = download_cpp_devtools_extension()
755
+ if extension_path:
756
+ print(
757
+ "✅ C++ DevTools Support extension ready for DWARF debugging!"
758
+ )
759
+ else:
760
+ print("⚠️ C++ DevTools Support extension download failed")
761
+ except Exception as e:
762
+ print(f"⚠️ Failed to setup C++ DevTools Support extension: {e}")
763
+
764
+ return True
765
+ else:
766
+ print(
767
+ f"❌ Failed to install Playwright browsers (exit code: {result.returncode})"
768
+ )
769
+ return False
770
+
771
+ except Exception as e:
772
+ print(f"❌ Error installing Playwright browsers: {e}")
773
+ return False