fastled 1.4.7__py3-none-any.whl → 1.4.9__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.
- fastled/__init__.py +2 -4
- fastled/__version__.py +1 -1
- fastled/args.py +3 -5
- fastled/chrome_extension_downloader.py +207 -0
- fastled/client_server.py +4 -14
- fastled/live_client.py +0 -3
- fastled/open_browser.py +17 -15
- fastled/parse_args.py +2 -2
- fastled/playwright_browser.py +267 -216
- {fastled-1.4.7.dist-info → fastled-1.4.9.dist-info}/METADATA +9 -28
- {fastled-1.4.7.dist-info → fastled-1.4.9.dist-info}/RECORD +15 -14
- {fastled-1.4.7.dist-info → fastled-1.4.9.dist-info}/WHEEL +0 -0
- {fastled-1.4.7.dist-info → fastled-1.4.9.dist-info}/entry_points.txt +0 -0
- {fastled-1.4.7.dist-info → fastled-1.4.9.dist-info}/licenses/LICENSE +0 -0
- {fastled-1.4.7.dist-info → fastled-1.4.9.dist-info}/top_level.txt +0 -0
fastled/__init__.py
CHANGED
@@ -81,7 +81,6 @@ class Api:
|
|
81
81
|
int | None
|
82
82
|
) = None, # None means auto select a free port. -1 means no server.
|
83
83
|
no_platformio: bool = False,
|
84
|
-
no_playwright: bool = False,
|
85
84
|
) -> LiveClient:
|
86
85
|
return LiveClient(
|
87
86
|
sketch_directory=sketch_directory,
|
@@ -94,7 +93,6 @@ class Api:
|
|
94
93
|
profile=profile,
|
95
94
|
http_port=http_port,
|
96
95
|
no_platformio=no_platformio,
|
97
|
-
no_playwright=no_playwright,
|
98
96
|
)
|
99
97
|
|
100
98
|
@staticmethod
|
@@ -219,7 +217,7 @@ class Test:
|
|
219
217
|
port: int | None = None,
|
220
218
|
compile_server_port: int | None = None,
|
221
219
|
open_browser: bool = True,
|
222
|
-
|
220
|
+
app: bool = False,
|
223
221
|
) -> Process:
|
224
222
|
from fastled.open_browser import spawn_http_server
|
225
223
|
|
@@ -231,7 +229,7 @@ class Test:
|
|
231
229
|
port=port,
|
232
230
|
compile_server_port=compile_server_port,
|
233
231
|
open_browser=open_browser,
|
234
|
-
|
232
|
+
app=app,
|
235
233
|
)
|
236
234
|
return proc
|
237
235
|
|
fastled/__version__.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# IMPORTANT! There's a bug in github which will REJECT any version update
|
2
2
|
# that has any other change in the repo. Please bump the version as the
|
3
3
|
# ONLY change in a commit, or else the pypi update and the release will fail.
|
4
|
-
__version__ = "1.4.
|
4
|
+
__version__ = "1.4.9"
|
5
5
|
|
6
6
|
__version_url_latest__ = "https://raw.githubusercontent.com/zackees/fastled-wasm/refs/heads/main/src/fastled/__version__.py"
|
fastled/args.py
CHANGED
@@ -13,7 +13,7 @@ class Args:
|
|
13
13
|
profile: bool
|
14
14
|
force_compile: bool
|
15
15
|
no_platformio: bool
|
16
|
-
|
16
|
+
app: bool # New flag to trigger Playwright browser with browser download if needed
|
17
17
|
auto_update: bool | None
|
18
18
|
update: bool
|
19
19
|
localhost: bool
|
@@ -52,9 +52,7 @@ class Args:
|
|
52
52
|
assert isinstance(
|
53
53
|
args.no_platformio, bool
|
54
54
|
), f"expected bool, got {type(args.no_platformio)}"
|
55
|
-
assert isinstance(
|
56
|
-
args.no_playwright, bool
|
57
|
-
), f"expected bool, got {type(args.no_playwright)}"
|
55
|
+
assert isinstance(args.app, bool), f"expected bool, got {type(args.app)}"
|
58
56
|
assert isinstance(
|
59
57
|
args.no_auto_updates, bool | None
|
60
58
|
), f"expected bool | None, got {type(args.no_auto_updates)}"
|
@@ -87,7 +85,7 @@ class Args:
|
|
87
85
|
profile=args.profile,
|
88
86
|
force_compile=args.force_compile,
|
89
87
|
no_platformio=args.no_platformio,
|
90
|
-
|
88
|
+
app=args.app,
|
91
89
|
auto_update=not args.no_auto_updates,
|
92
90
|
update=args.update,
|
93
91
|
localhost=args.localhost,
|
@@ -0,0 +1,207 @@
|
|
1
|
+
"""
|
2
|
+
Chrome extension downloader utility for FastLED WASM compiler.
|
3
|
+
|
4
|
+
This module provides functionality to download Chrome extensions from the
|
5
|
+
Chrome Web Store and prepare them for use with Playwright browser.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
import re
|
10
|
+
import shutil
|
11
|
+
import tempfile
|
12
|
+
import warnings
|
13
|
+
import zipfile
|
14
|
+
from pathlib import Path
|
15
|
+
|
16
|
+
import httpx
|
17
|
+
|
18
|
+
|
19
|
+
class ChromeExtensionDownloader:
|
20
|
+
"""Downloads Chrome extensions from the Chrome Web Store."""
|
21
|
+
|
22
|
+
# Chrome Web Store CRX download URL
|
23
|
+
CRX_URL = "https://clients2.google.com/service/update2/crx?response=redirect&prodversion=114.0&acceptformat=crx2,crx3&x=id%3D{extension_id}%26uc"
|
24
|
+
|
25
|
+
# Modern user agent string
|
26
|
+
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"
|
27
|
+
|
28
|
+
def __init__(self, cache_dir: Path | None = None):
|
29
|
+
"""Initialize the Chrome extension downloader.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
cache_dir: Directory to store downloaded extensions.
|
33
|
+
Defaults to ~/.fastled/chrome-extensions
|
34
|
+
"""
|
35
|
+
if cache_dir is None:
|
36
|
+
cache_dir = Path.home() / ".fastled" / "chrome-extensions"
|
37
|
+
|
38
|
+
self.cache_dir = Path(cache_dir)
|
39
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
40
|
+
|
41
|
+
self.headers = {
|
42
|
+
"User-Agent": self.USER_AGENT,
|
43
|
+
"Referer": "https://chrome.google.com",
|
44
|
+
}
|
45
|
+
|
46
|
+
def extract_extension_id(self, url: str) -> str:
|
47
|
+
"""Extract extension ID from Chrome Web Store URL.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
url: Chrome Web Store URL
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
Extension ID string
|
54
|
+
|
55
|
+
Raises:
|
56
|
+
ValueError: If URL is not a valid Chrome Web Store URL
|
57
|
+
"""
|
58
|
+
# Match new Chrome Web Store URLs (chromewebstore.google.com)
|
59
|
+
new_pattern = r"chromewebstore\.google\.com/detail/[^/]+/([a-z]{32})"
|
60
|
+
match = re.search(new_pattern, url)
|
61
|
+
|
62
|
+
if match:
|
63
|
+
return match.group(1)
|
64
|
+
|
65
|
+
# Match old Chrome Web Store URLs (chrome.google.com/webstore)
|
66
|
+
old_pattern = r"chrome\.google\.com/webstore/detail/[^/]+/([a-z]{32})"
|
67
|
+
match = re.search(old_pattern, url)
|
68
|
+
|
69
|
+
if match:
|
70
|
+
return match.group(1)
|
71
|
+
|
72
|
+
# Try direct extension ID
|
73
|
+
if re.match(r"^[a-z]{32}$", url):
|
74
|
+
return url
|
75
|
+
|
76
|
+
raise ValueError(f"Invalid Chrome Web Store URL or extension ID: {url}")
|
77
|
+
|
78
|
+
def download_crx(self, extension_id: str) -> bytes:
|
79
|
+
"""Download CRX file from Chrome Web Store.
|
80
|
+
|
81
|
+
Args:
|
82
|
+
extension_id: Chrome extension ID
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
CRX file content as bytes
|
86
|
+
|
87
|
+
Raises:
|
88
|
+
httpx.RequestError: If download fails
|
89
|
+
"""
|
90
|
+
download_url = self.CRX_URL.format(extension_id=extension_id)
|
91
|
+
|
92
|
+
with httpx.Client(follow_redirects=True) as client:
|
93
|
+
response = client.get(download_url, headers=self.headers)
|
94
|
+
response.raise_for_status()
|
95
|
+
|
96
|
+
return response.content
|
97
|
+
|
98
|
+
def extract_crx_to_directory(self, crx_content: bytes, extract_dir: Path) -> None:
|
99
|
+
"""Extract CRX file content to a directory.
|
100
|
+
|
101
|
+
CRX files are essentially ZIP files with a header that needs to be removed.
|
102
|
+
|
103
|
+
Args:
|
104
|
+
crx_content: CRX file content as bytes
|
105
|
+
extract_dir: Directory to extract the extension to
|
106
|
+
"""
|
107
|
+
# CRX files have a header before the ZIP content
|
108
|
+
# We need to find the ZIP header (starts with 'PK')
|
109
|
+
zip_start = crx_content.find(b"PK\x03\x04")
|
110
|
+
if zip_start == -1:
|
111
|
+
zip_start = crx_content.find(b"PK\x05\x06") # Empty ZIP
|
112
|
+
|
113
|
+
if zip_start == -1:
|
114
|
+
raise ValueError("Could not find ZIP header in CRX file")
|
115
|
+
|
116
|
+
# Extract the ZIP portion
|
117
|
+
zip_content = crx_content[zip_start:]
|
118
|
+
|
119
|
+
# Create temporary file to extract from
|
120
|
+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as temp_zip:
|
121
|
+
temp_zip.write(zip_content)
|
122
|
+
temp_zip_path = temp_zip.name
|
123
|
+
|
124
|
+
try:
|
125
|
+
# Extract the ZIP file
|
126
|
+
extract_dir.mkdir(parents=True, exist_ok=True)
|
127
|
+
with zipfile.ZipFile(temp_zip_path, "r") as zip_ref:
|
128
|
+
zip_ref.extractall(extract_dir)
|
129
|
+
finally:
|
130
|
+
# Clean up temporary file
|
131
|
+
os.unlink(temp_zip_path)
|
132
|
+
|
133
|
+
def get_extension_path(
|
134
|
+
self, url_or_id: str, extension_name: str | None = None
|
135
|
+
) -> Path:
|
136
|
+
"""Download and extract Chrome extension, returning the path to the extracted directory.
|
137
|
+
|
138
|
+
Args:
|
139
|
+
url_or_id: Chrome Web Store URL or extension ID
|
140
|
+
extension_name: Optional name for the extension directory
|
141
|
+
|
142
|
+
Returns:
|
143
|
+
Path to the extracted extension directory
|
144
|
+
"""
|
145
|
+
extension_id = self.extract_extension_id(url_or_id)
|
146
|
+
|
147
|
+
if extension_name is None:
|
148
|
+
extension_name = extension_id
|
149
|
+
|
150
|
+
extension_dir = self.cache_dir / extension_name
|
151
|
+
|
152
|
+
# Check if extension is already downloaded and extracted
|
153
|
+
if extension_dir.exists() and (extension_dir / "manifest.json").exists():
|
154
|
+
print(f"✅ Chrome extension already cached: {extension_dir}")
|
155
|
+
return extension_dir
|
156
|
+
|
157
|
+
print(f"🔽 Downloading Chrome extension {extension_id}...")
|
158
|
+
|
159
|
+
try:
|
160
|
+
# Download the CRX file
|
161
|
+
crx_content = self.download_crx(extension_id)
|
162
|
+
|
163
|
+
# Clean up existing directory if it exists
|
164
|
+
if extension_dir.exists():
|
165
|
+
shutil.rmtree(extension_dir)
|
166
|
+
|
167
|
+
# Extract the CRX file
|
168
|
+
self.extract_crx_to_directory(crx_content, extension_dir)
|
169
|
+
|
170
|
+
# Verify extraction worked
|
171
|
+
if not (extension_dir / "manifest.json").exists():
|
172
|
+
raise ValueError("Extension extraction failed - no manifest.json found")
|
173
|
+
|
174
|
+
print(f"✅ Chrome extension downloaded and extracted: {extension_dir}")
|
175
|
+
return extension_dir
|
176
|
+
|
177
|
+
except Exception as e:
|
178
|
+
warnings.warn(f"Failed to download Chrome extension {extension_id}: {e}")
|
179
|
+
if extension_dir.exists():
|
180
|
+
shutil.rmtree(extension_dir)
|
181
|
+
raise
|
182
|
+
|
183
|
+
|
184
|
+
def download_cpp_devtools_extension() -> Path | None:
|
185
|
+
"""Download the C++ DevTools Support (DWARF) extension.
|
186
|
+
|
187
|
+
Returns:
|
188
|
+
Path to the extracted extension directory, or None if download failed
|
189
|
+
"""
|
190
|
+
# C++ DevTools Support (DWARF) extension
|
191
|
+
extension_url = "https://chromewebstore.google.com/detail/cc++-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb"
|
192
|
+
|
193
|
+
try:
|
194
|
+
downloader = ChromeExtensionDownloader()
|
195
|
+
return downloader.get_extension_path(extension_url, "cpp-devtools-support")
|
196
|
+
except Exception as e:
|
197
|
+
warnings.warn(f"Failed to download C++ DevTools Support extension: {e}")
|
198
|
+
return None
|
199
|
+
|
200
|
+
|
201
|
+
if __name__ == "__main__":
|
202
|
+
# Test the downloader with the C++ DevTools Support extension
|
203
|
+
extension_path = download_cpp_devtools_extension()
|
204
|
+
if extension_path:
|
205
|
+
print(f"Extension downloaded to: {extension_path}")
|
206
|
+
else:
|
207
|
+
print("Failed to download extension")
|
fastled/client_server.py
CHANGED
@@ -25,16 +25,6 @@ from fastled.web_compile import (
|
|
25
25
|
)
|
26
26
|
|
27
27
|
|
28
|
-
def _always_false() -> bool:
|
29
|
-
return False
|
30
|
-
|
31
|
-
|
32
|
-
try:
|
33
|
-
from fastled.playwright_browser import is_playwright_available
|
34
|
-
except ImportError:
|
35
|
-
is_playwright_available = _always_false
|
36
|
-
|
37
|
-
|
38
28
|
def _create_error_html(error_message: str) -> str:
|
39
29
|
return f"""<!DOCTYPE html>
|
40
30
|
<html>
|
@@ -277,7 +267,7 @@ def run_client(
|
|
277
267
|
) = None, # None means auto select a free port, http_port < 0 means no server.
|
278
268
|
clear: bool = False,
|
279
269
|
no_platformio: bool = False,
|
280
|
-
|
270
|
+
app: bool = False, # Use app-like browser experience
|
281
271
|
) -> int:
|
282
272
|
has_checked_newer_version_yet = False
|
283
273
|
compile_server: CompileServer | None = None
|
@@ -365,7 +355,7 @@ def run_client(
|
|
365
355
|
port=http_port,
|
366
356
|
compile_server_port=port,
|
367
357
|
open_browser=open_web_browser,
|
368
|
-
|
358
|
+
app=app,
|
369
359
|
)
|
370
360
|
else:
|
371
361
|
print("\nCompilation successful.")
|
@@ -531,7 +521,7 @@ def run_client_server(args: Args) -> int:
|
|
531
521
|
open_web_browser = not just_compile and not interactive
|
532
522
|
build_mode: BuildMode = BuildMode.from_args(args)
|
533
523
|
no_platformio = bool(args.no_platformio)
|
534
|
-
|
524
|
+
app = bool(args.app)
|
535
525
|
|
536
526
|
if not force_compile and not looks_like_sketch_directory(directory):
|
537
527
|
# if there is only one directory in the sketch directory, use that
|
@@ -594,7 +584,7 @@ def run_client_server(args: Args) -> int:
|
|
594
584
|
profile=profile,
|
595
585
|
clear=args.clear,
|
596
586
|
no_platformio=no_platformio,
|
597
|
-
|
587
|
+
app=app,
|
598
588
|
)
|
599
589
|
except KeyboardInterrupt:
|
600
590
|
return 1
|
fastled/live_client.py
CHANGED
@@ -23,7 +23,6 @@ class LiveClient:
|
|
23
23
|
build_mode: BuildMode = BuildMode.QUICK,
|
24
24
|
profile: bool = False,
|
25
25
|
no_platformio: bool = False,
|
26
|
-
no_playwright: bool = False,
|
27
26
|
) -> None:
|
28
27
|
self.sketch_directory = sketch_directory
|
29
28
|
self.host = host
|
@@ -37,7 +36,6 @@ class LiveClient:
|
|
37
36
|
self.thread: threading.Thread | None = None
|
38
37
|
self.auto_updates = auto_updates
|
39
38
|
self.no_platformio = no_platformio
|
40
|
-
self.no_playwright = no_playwright
|
41
39
|
if auto_start:
|
42
40
|
self.start()
|
43
41
|
if self.auto_updates is False:
|
@@ -57,7 +55,6 @@ class LiveClient:
|
|
57
55
|
shutdown=self.shutdown,
|
58
56
|
http_port=self.http_port,
|
59
57
|
no_platformio=self.no_platformio,
|
60
|
-
no_playwright=self.no_playwright,
|
61
58
|
)
|
62
59
|
return rtn
|
63
60
|
|
fastled/open_browser.py
CHANGED
@@ -6,16 +6,9 @@ import weakref
|
|
6
6
|
from multiprocessing import Process
|
7
7
|
from pathlib import Path
|
8
8
|
|
9
|
+
from fastled.playwright_browser import open_with_playwright
|
9
10
|
from fastled.server_flask import run_flask_in_thread
|
10
11
|
|
11
|
-
try:
|
12
|
-
from fastled.playwright_browser import is_playwright_available, open_with_playwright
|
13
|
-
|
14
|
-
PLAYWRIGHT_AVAILABLE = is_playwright_available()
|
15
|
-
except ImportError:
|
16
|
-
PLAYWRIGHT_AVAILABLE = False
|
17
|
-
open_with_playwright = None
|
18
|
-
|
19
12
|
# Global reference to keep Playwright browser alive
|
20
13
|
_playwright_browser_proxy = None
|
21
14
|
|
@@ -101,7 +94,7 @@ def spawn_http_server(
|
|
101
94
|
compile_server_port: int,
|
102
95
|
port: int | None = None,
|
103
96
|
open_browser: bool = True,
|
104
|
-
|
97
|
+
app: bool = False,
|
105
98
|
) -> Process:
|
106
99
|
|
107
100
|
if port is not None and not is_port_free(port):
|
@@ -130,17 +123,26 @@ def spawn_http_server(
|
|
130
123
|
wait_for_server(port)
|
131
124
|
if open_browser:
|
132
125
|
url = f"http://localhost:{port}"
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
126
|
+
should_use_playwright = app
|
127
|
+
|
128
|
+
if should_use_playwright:
|
129
|
+
if app:
|
130
|
+
# For --app mode, try to install browsers if needed
|
131
|
+
from fastled.playwright_browser import install_playwright_browsers
|
132
|
+
|
133
|
+
install_playwright_browsers()
|
134
|
+
|
138
135
|
print(f"Opening FastLED sketch in Playwright browser: {url}")
|
139
136
|
print(
|
140
137
|
"Auto-resize enabled: Browser window will automatically adjust to content size"
|
141
138
|
)
|
139
|
+
print(
|
140
|
+
"🔧 C++ DevTools Support extension will be loaded for DWARF debugging"
|
141
|
+
)
|
142
142
|
global _playwright_browser_proxy
|
143
|
-
_playwright_browser_proxy = open_with_playwright(
|
143
|
+
_playwright_browser_proxy = open_with_playwright(
|
144
|
+
url, enable_extensions=True
|
145
|
+
)
|
144
146
|
else:
|
145
147
|
print(f"Opening browser to {url}")
|
146
148
|
import webbrowser
|
fastled/parse_args.py
CHANGED
@@ -121,9 +121,9 @@ def parse_args() -> Args:
|
|
121
121
|
help="Bypass PlatformIO constraints by using local Docker compilation with custom build environment",
|
122
122
|
)
|
123
123
|
parser.add_argument(
|
124
|
-
"--
|
124
|
+
"--app",
|
125
125
|
action="store_true",
|
126
|
-
help="
|
126
|
+
help="Use Playwright app-like browser experience (will download browsers if needed)",
|
127
127
|
)
|
128
128
|
parser.add_argument(
|
129
129
|
"-u",
|
fastled/playwright_browser.py
CHANGED
@@ -18,21 +18,6 @@ from typing import Any
|
|
18
18
|
PLAYWRIGHT_DIR = Path.home() / ".fastled" / "playwright"
|
19
19
|
PLAYWRIGHT_DIR.mkdir(parents=True, exist_ok=True)
|
20
20
|
|
21
|
-
try:
|
22
|
-
from playwright.async_api import Browser, Page, async_playwright
|
23
|
-
|
24
|
-
PLAYWRIGHT_AVAILABLE = True
|
25
|
-
except ImportError:
|
26
|
-
PLAYWRIGHT_AVAILABLE = False
|
27
|
-
async_playwright = None
|
28
|
-
Browser = None
|
29
|
-
Page = None
|
30
|
-
|
31
|
-
|
32
|
-
def is_playwright_available() -> bool:
|
33
|
-
"""Check if Playwright is available."""
|
34
|
-
return PLAYWRIGHT_AVAILABLE
|
35
|
-
|
36
21
|
|
37
22
|
def get_chromium_executable_path() -> str | None:
|
38
23
|
"""Get the path to the custom Chromium executable if it exists."""
|
@@ -67,23 +52,47 @@ def get_chromium_executable_path() -> str | None:
|
|
67
52
|
class PlaywrightBrowser:
|
68
53
|
"""Playwright browser manager for FastLED sketches."""
|
69
54
|
|
70
|
-
def __init__(self, headless: bool = False):
|
55
|
+
def __init__(self, headless: bool = False, enable_extensions: bool = True):
|
71
56
|
"""Initialize the Playwright browser manager.
|
72
57
|
|
73
58
|
Args:
|
74
59
|
headless: Whether to run the browser in headless mode
|
60
|
+
enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
|
75
61
|
"""
|
76
|
-
if not is_playwright_available():
|
77
|
-
raise ImportError(
|
78
|
-
"Playwright is not installed. Install with: pip install fastled[full]"
|
79
|
-
)
|
80
62
|
|
81
63
|
self.headless = headless
|
64
|
+
self.enable_extensions = enable_extensions
|
82
65
|
self.auto_resize = True # Always enable auto-resize
|
83
66
|
self.browser: Any = None
|
67
|
+
self.context: Any = None
|
84
68
|
self.page: Any = None
|
85
69
|
self.playwright: Any = None
|
86
70
|
self._should_exit = asyncio.Event()
|
71
|
+
self._extensions_dir: Path | None = None
|
72
|
+
|
73
|
+
# Initialize extensions if enabled
|
74
|
+
if self.enable_extensions:
|
75
|
+
self._setup_extensions()
|
76
|
+
|
77
|
+
def _setup_extensions(self) -> None:
|
78
|
+
"""Setup Chrome extensions for enhanced debugging."""
|
79
|
+
try:
|
80
|
+
from fastled.chrome_extension_downloader import (
|
81
|
+
download_cpp_devtools_extension,
|
82
|
+
)
|
83
|
+
|
84
|
+
extension_path = download_cpp_devtools_extension()
|
85
|
+
if extension_path and extension_path.exists():
|
86
|
+
self._extensions_dir = extension_path
|
87
|
+
print(
|
88
|
+
f"[PYTHON] C++ DevTools Support extension ready: {extension_path}"
|
89
|
+
)
|
90
|
+
else:
|
91
|
+
print("[PYTHON] Warning: C++ DevTools Support extension not available")
|
92
|
+
self.enable_extensions = False
|
93
|
+
except Exception as e:
|
94
|
+
print(f"[PYTHON] Warning: Failed to setup Chrome extensions: {e}")
|
95
|
+
self.enable_extensions = False
|
87
96
|
|
88
97
|
def _detect_device_scale_factor(self) -> float | None:
|
89
98
|
"""Detect the system's device scale factor for natural browser behavior.
|
@@ -93,58 +102,71 @@ class PlaywrightBrowser:
|
|
93
102
|
the value is outside reasonable bounds (0.5-4.0).
|
94
103
|
"""
|
95
104
|
try:
|
96
|
-
import
|
105
|
+
import tkinter
|
97
106
|
|
98
|
-
|
99
|
-
|
107
|
+
root = tkinter.Tk()
|
108
|
+
root.withdraw() # Hide the window
|
109
|
+
scale_factor = root.winfo_fpixels("1i") / 72.0
|
110
|
+
root.destroy()
|
100
111
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
)
|
105
|
-
dpi, _ = winreg.QueryValueEx(key, "AppliedDPI")
|
106
|
-
winreg.CloseKey(key)
|
107
|
-
device_scale_factor = dpi / 96.0
|
108
|
-
|
109
|
-
# Validate the scale factor is within reasonable bounds
|
110
|
-
if 0.5 <= device_scale_factor <= 4.0:
|
111
|
-
print(
|
112
|
-
f"[PYTHON] Detected Windows DPI scaling: {device_scale_factor:.2f}"
|
113
|
-
)
|
114
|
-
return device_scale_factor
|
115
|
-
else:
|
116
|
-
print(
|
117
|
-
f"[PYTHON] Detected scale factor {device_scale_factor:.2f} outside reasonable bounds"
|
118
|
-
)
|
119
|
-
return None
|
120
|
-
|
121
|
-
except (OSError, FileNotFoundError):
|
122
|
-
print(
|
123
|
-
"[PYTHON] Could not detect Windows DPI, using browser default"
|
124
|
-
)
|
125
|
-
return None
|
112
|
+
# Validate the scale factor is in a reasonable range
|
113
|
+
if 0.5 <= scale_factor <= 4.0:
|
114
|
+
return scale_factor
|
126
115
|
else:
|
127
|
-
# Future: Add support for other platforms (macOS, Linux) here
|
128
116
|
print(
|
129
|
-
f"[PYTHON]
|
117
|
+
f"[PYTHON] Detected scale factor {scale_factor:.2f} is outside reasonable bounds (0.5-4.0)"
|
130
118
|
)
|
131
119
|
return None
|
132
120
|
|
133
121
|
except Exception as e:
|
134
|
-
print(f"[PYTHON]
|
122
|
+
print(f"[PYTHON] Could not detect device scale factor: {e}")
|
135
123
|
return None
|
136
124
|
|
137
125
|
async def start(self) -> None:
|
138
126
|
"""Start the Playwright browser."""
|
139
|
-
if self.browser is None:
|
140
|
-
|
141
|
-
|
127
|
+
if self.browser is None and self.context is None:
|
128
|
+
|
129
|
+
from playwright.async_api import async_playwright
|
130
|
+
|
131
|
+
self.playwright = async_playwright()
|
132
|
+
playwright = await self.playwright.start()
|
142
133
|
|
143
|
-
|
144
|
-
|
134
|
+
if self.enable_extensions and self._extensions_dir:
|
135
|
+
# Use persistent context for extensions
|
136
|
+
user_data_dir = PLAYWRIGHT_DIR / "user-data"
|
137
|
+
user_data_dir.mkdir(parents=True, exist_ok=True)
|
138
|
+
|
139
|
+
launch_kwargs = {
|
140
|
+
"headless": False, # Extensions require headed mode
|
141
|
+
"channel": "chromium", # Required for extensions
|
142
|
+
"args": [
|
143
|
+
"--disable-dev-shm-usage",
|
144
|
+
"--disable-web-security",
|
145
|
+
"--allow-running-insecure-content",
|
146
|
+
f"--disable-extensions-except={self._extensions_dir}",
|
147
|
+
f"--load-extension={self._extensions_dir}",
|
148
|
+
],
|
149
|
+
}
|
145
150
|
|
146
151
|
# Get custom Chromium executable path if available
|
147
152
|
executable_path = get_chromium_executable_path()
|
153
|
+
if executable_path:
|
154
|
+
launch_kwargs["executable_path"] = executable_path
|
155
|
+
print(
|
156
|
+
f"[PYTHON] Using custom Chromium executable: {executable_path}"
|
157
|
+
)
|
158
|
+
|
159
|
+
self.context = await playwright.chromium.launch_persistent_context(
|
160
|
+
str(user_data_dir), **launch_kwargs
|
161
|
+
)
|
162
|
+
|
163
|
+
print(
|
164
|
+
"[PYTHON] Started Playwright browser with C++ DevTools Support extension"
|
165
|
+
)
|
166
|
+
|
167
|
+
else:
|
168
|
+
# Regular browser launch without extensions
|
169
|
+
executable_path = get_chromium_executable_path()
|
148
170
|
launch_kwargs = {
|
149
171
|
"headless": self.headless,
|
150
172
|
"args": [
|
@@ -161,28 +183,34 @@ class PlaywrightBrowser:
|
|
161
183
|
)
|
162
184
|
|
163
185
|
self.browser = await playwright.chromium.launch(**launch_kwargs)
|
164
|
-
else:
|
165
|
-
raise RuntimeError("Playwright is not available")
|
166
186
|
|
167
|
-
if self.page is None
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
187
|
+
if self.page is None:
|
188
|
+
if self.context:
|
189
|
+
# Using persistent context (with extensions)
|
190
|
+
if len(self.context.pages) > 0:
|
191
|
+
self.page = self.context.pages[0]
|
192
|
+
else:
|
193
|
+
self.page = await self.context.new_page()
|
194
|
+
elif self.browser:
|
195
|
+
# Using regular browser
|
196
|
+
# Detect system device scale factor for natural browser behavior
|
197
|
+
device_scale_factor = self._detect_device_scale_factor()
|
198
|
+
|
199
|
+
# Create browser context with detected or default device scale factor
|
200
|
+
if device_scale_factor:
|
201
|
+
context = await self.browser.new_context(
|
202
|
+
device_scale_factor=device_scale_factor
|
203
|
+
)
|
204
|
+
print(
|
205
|
+
f"[PYTHON] Created browser context with device scale factor: {device_scale_factor:.2f}"
|
206
|
+
)
|
207
|
+
else:
|
208
|
+
context = await self.browser.new_context()
|
209
|
+
print(
|
210
|
+
"[PYTHON] Created browser context with default device scale factor"
|
211
|
+
)
|
184
212
|
|
185
|
-
|
213
|
+
self.page = await context.new_page()
|
186
214
|
|
187
215
|
async def open_url(self, url: str) -> None:
|
188
216
|
"""Open a URL in the Playwright browser.
|
@@ -213,6 +241,33 @@ class PlaywrightBrowser:
|
|
213
241
|
if self.auto_resize:
|
214
242
|
await self._setup_auto_resize()
|
215
243
|
|
244
|
+
# Check if C++ DevTools extension is loaded
|
245
|
+
if self.enable_extensions and self._extensions_dir:
|
246
|
+
try:
|
247
|
+
# Check if the extension is available in the DevTools
|
248
|
+
extensions_available = await self.page.evaluate(
|
249
|
+
"""
|
250
|
+
() => {
|
251
|
+
// Check if chrome.devtools is available (extension context)
|
252
|
+
return typeof chrome !== 'undefined' &&
|
253
|
+
typeof chrome.runtime !== 'undefined' &&
|
254
|
+
chrome.runtime.id !== undefined;
|
255
|
+
}
|
256
|
+
"""
|
257
|
+
)
|
258
|
+
|
259
|
+
if extensions_available:
|
260
|
+
print(
|
261
|
+
"[PYTHON] ✅ C++ DevTools Support extension is active and ready for DWARF debugging"
|
262
|
+
)
|
263
|
+
else:
|
264
|
+
print(
|
265
|
+
"[PYTHON] ⚠️ C++ DevTools Support extension may not be fully loaded"
|
266
|
+
)
|
267
|
+
|
268
|
+
except Exception as e:
|
269
|
+
print(f"[PYTHON] Could not verify extension status: {e}")
|
270
|
+
|
216
271
|
async def _setup_auto_resize(self) -> None:
|
217
272
|
"""Set up automatic window resizing based on content size."""
|
218
273
|
if self.page is None:
|
@@ -226,119 +281,92 @@ class PlaywrightBrowser:
|
|
226
281
|
# Start polling loop that tracks browser window changes and adjusts viewport only
|
227
282
|
asyncio.create_task(self._track_browser_adjust_viewport())
|
228
283
|
|
229
|
-
async def
|
230
|
-
"""
|
284
|
+
async def _track_browser_adjust_viewport(self) -> None:
|
285
|
+
"""Track browser window changes and adjust viewport accordingly.
|
231
286
|
|
232
|
-
|
233
|
-
|
287
|
+
This method polls for changes in the browser window size and adjusts
|
288
|
+
the viewport size to match, maintaining the browser window dimensions
|
289
|
+
while ensuring the content area matches the sketch requirements.
|
234
290
|
"""
|
235
|
-
|
236
|
-
|
291
|
+
last_viewport = None
|
292
|
+
consecutive_same_count = 0
|
293
|
+
max_consecutive_same = 5
|
237
294
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
295
|
+
while not self._should_exit.is_set():
|
296
|
+
try:
|
297
|
+
await asyncio.sleep(0.5) # Poll every 500ms
|
298
|
+
|
299
|
+
if self.page is None:
|
300
|
+
continue
|
301
|
+
|
302
|
+
# Get current viewport size
|
303
|
+
current_viewport = await self.page.evaluate(
|
304
|
+
"""
|
305
|
+
() => ({
|
306
|
+
width: window.innerWidth,
|
307
|
+
height: window.innerHeight
|
308
|
+
})
|
251
309
|
"""
|
252
|
-
|
253
|
-
except Exception:
|
254
|
-
return None
|
255
|
-
|
256
|
-
async def _track_browser_adjust_viewport(self) -> None:
|
257
|
-
"""Track browser window outer size changes and adjust viewport accordingly."""
|
258
|
-
if self.page is None:
|
259
|
-
return
|
310
|
+
)
|
260
311
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
312
|
+
# Check if viewport changed
|
313
|
+
if current_viewport != last_viewport:
|
314
|
+
last_viewport = current_viewport
|
315
|
+
consecutive_same_count = 0
|
265
316
|
|
266
|
-
|
267
|
-
|
268
|
-
# Wait 1 second between polls
|
269
|
-
await asyncio.sleep(1)
|
270
|
-
|
271
|
-
# Check if page is still alive
|
272
|
-
if self.page is None or self.page.is_closed():
|
273
|
-
print("[PYTHON] Page closed, signaling exit")
|
274
|
-
self._should_exit.set()
|
275
|
-
return
|
276
|
-
|
277
|
-
# Get browser window dimensions
|
278
|
-
window_info = await self._get_window_info()
|
279
|
-
|
280
|
-
if window_info:
|
281
|
-
current_outer = (
|
282
|
-
window_info["outerWidth"],
|
283
|
-
window_info["outerHeight"],
|
317
|
+
print(
|
318
|
+
f"[PYTHON] Viewport: {current_viewport['width']}x{current_viewport['height']}"
|
284
319
|
)
|
285
320
|
|
286
|
-
#
|
287
|
-
|
288
|
-
|
289
|
-
|
321
|
+
# Try to get window outer dimensions for context
|
322
|
+
try:
|
323
|
+
window_info = await self.page.evaluate(
|
324
|
+
"""
|
325
|
+
() => ({
|
326
|
+
outerWidth: window.outerWidth,
|
327
|
+
outerHeight: window.outerHeight,
|
328
|
+
screenX: window.screenX,
|
329
|
+
screenY: window.screenY
|
330
|
+
})
|
331
|
+
"""
|
290
332
|
)
|
291
333
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
if last_outer_size is not None:
|
296
|
-
print("[PYTHON] *** BROWSER WINDOW RESIZED ***")
|
297
|
-
print(
|
298
|
-
f"[PYTHON] Outer window changed from {last_outer_size[0]}x{last_outer_size[1]} to {current_outer[0]}x{current_outer[1]}"
|
299
|
-
)
|
300
|
-
|
301
|
-
last_outer_size = current_outer
|
302
|
-
|
303
|
-
# Set viewport to match the outer window size
|
304
|
-
if not self.headless:
|
305
|
-
try:
|
306
|
-
outer_width = int(window_info["outerWidth"])
|
307
|
-
outer_height = int(window_info["outerHeight"])
|
308
|
-
|
309
|
-
print(
|
310
|
-
f"[PYTHON] Setting viewport to match outer window size: {outer_width}x{outer_height}"
|
311
|
-
)
|
312
|
-
|
313
|
-
await self.page.set_viewport_size(
|
314
|
-
{"width": outer_width, "height": outer_height}
|
315
|
-
)
|
316
|
-
print("[PYTHON] Viewport set successfully")
|
317
|
-
|
318
|
-
# Wait briefly for browser to settle after viewport change
|
319
|
-
# await asyncio.sleep(0.5)
|
334
|
+
print(
|
335
|
+
f"[PYTHON] Window: {window_info['outerWidth']}x{window_info['outerHeight']} at ({window_info['screenX']}, {window_info['screenY']})"
|
336
|
+
)
|
320
337
|
|
321
|
-
|
322
|
-
|
338
|
+
except Exception:
|
339
|
+
pass
|
323
340
|
|
324
|
-
|
341
|
+
else:
|
342
|
+
consecutive_same_count += 1
|
343
|
+
if consecutive_same_count >= max_consecutive_same:
|
344
|
+
# Viewport hasn't changed for a while, reduce polling frequency
|
345
|
+
await asyncio.sleep(2.0)
|
346
|
+
consecutive_same_count = 0
|
325
347
|
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
348
|
+
# Get the browser window information periodically
|
349
|
+
try:
|
350
|
+
browser_info = await self.page.evaluate(
|
351
|
+
"""
|
352
|
+
() => {
|
353
|
+
return {
|
354
|
+
userAgent: navigator.userAgent,
|
355
|
+
platform: navigator.platform,
|
356
|
+
cookieEnabled: navigator.cookieEnabled,
|
357
|
+
language: navigator.language
|
358
|
+
};
|
359
|
+
}
|
360
|
+
"""
|
361
|
+
)
|
336
362
|
|
337
|
-
|
338
|
-
|
363
|
+
if browser_info:
|
364
|
+
pass # We have browser info, but don't need to print it constantly
|
365
|
+
else:
|
366
|
+
print("[PYTHON] Could not get browser window info")
|
339
367
|
|
340
|
-
|
341
|
-
print("[PYTHON] Could not get browser
|
368
|
+
except Exception as e:
|
369
|
+
print(f"[PYTHON] Could not get browser info: {e}")
|
342
370
|
|
343
371
|
except Exception as e:
|
344
372
|
print(f"[PYTHON] Error in browser tracking: {e}")
|
@@ -346,15 +374,20 @@ class PlaywrightBrowser:
|
|
346
374
|
|
347
375
|
async def wait_for_close(self) -> None:
|
348
376
|
"""Wait for the browser to be closed."""
|
349
|
-
if self.
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
377
|
+
if self.context:
|
378
|
+
# Wait for persistent context to be closed
|
379
|
+
try:
|
380
|
+
while not self.context.closed:
|
381
|
+
await asyncio.sleep(1)
|
382
|
+
except Exception:
|
383
|
+
pass
|
384
|
+
elif self.browser:
|
385
|
+
try:
|
386
|
+
# Wait for the browser to be closed
|
387
|
+
while not self.browser.is_closed():
|
388
|
+
await asyncio.sleep(1)
|
389
|
+
except Exception:
|
390
|
+
pass
|
358
391
|
|
359
392
|
async def close(self) -> None:
|
360
393
|
"""Close the Playwright browser."""
|
@@ -362,6 +395,10 @@ class PlaywrightBrowser:
|
|
362
395
|
await self.page.close()
|
363
396
|
self.page = None
|
364
397
|
|
398
|
+
if self.context:
|
399
|
+
await self.context.close()
|
400
|
+
self.context = None
|
401
|
+
|
365
402
|
if self.browser:
|
366
403
|
await self.browser.close()
|
367
404
|
self.browser = None
|
@@ -371,27 +408,23 @@ class PlaywrightBrowser:
|
|
371
408
|
self.playwright = None
|
372
409
|
|
373
410
|
|
374
|
-
def run_playwright_browser(
|
411
|
+
def run_playwright_browser(
|
412
|
+
url: str, headless: bool = False, enable_extensions: bool = True
|
413
|
+
) -> None:
|
375
414
|
"""Run Playwright browser in a separate process.
|
376
415
|
|
377
416
|
Args:
|
378
417
|
url: The URL to open
|
379
418
|
headless: Whether to run in headless mode
|
419
|
+
enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
|
380
420
|
"""
|
381
|
-
if not is_playwright_available():
|
382
|
-
warnings.warn(
|
383
|
-
"Playwright is not installed. Install with: pip install fastled[full]. "
|
384
|
-
"Falling back to default browser."
|
385
|
-
)
|
386
|
-
import webbrowser
|
387
|
-
|
388
|
-
webbrowser.open(url)
|
389
|
-
return
|
390
421
|
|
391
422
|
async def main():
|
392
423
|
browser = None
|
393
424
|
try:
|
394
|
-
browser = PlaywrightBrowser(
|
425
|
+
browser = PlaywrightBrowser(
|
426
|
+
headless=headless, enable_extensions=enable_extensions
|
427
|
+
)
|
395
428
|
await browser.start()
|
396
429
|
await browser.open_url(url)
|
397
430
|
|
@@ -405,7 +438,9 @@ def run_playwright_browser(url: str, headless: bool = False) -> None:
|
|
405
438
|
if install_playwright_browsers():
|
406
439
|
print("🎭 Retrying browser startup...")
|
407
440
|
# Try again with fresh browser instance
|
408
|
-
browser = PlaywrightBrowser(
|
441
|
+
browser = PlaywrightBrowser(
|
442
|
+
headless=headless, enable_extensions=enable_extensions
|
443
|
+
)
|
409
444
|
await browser.start()
|
410
445
|
await browser.open_url(url)
|
411
446
|
|
@@ -445,23 +480,16 @@ class PlaywrightBrowserProxy:
|
|
445
480
|
self.monitor_thread = None
|
446
481
|
self._closing_intentionally = False
|
447
482
|
|
448
|
-
def open(
|
483
|
+
def open(
|
484
|
+
self, url: str, headless: bool = False, enable_extensions: bool = True
|
485
|
+
) -> None:
|
449
486
|
"""Open URL with Playwright browser and keep it alive.
|
450
487
|
|
451
488
|
Args:
|
452
489
|
url: The URL to open
|
453
490
|
headless: Whether to run in headless mode
|
491
|
+
enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
|
454
492
|
"""
|
455
|
-
if not is_playwright_available():
|
456
|
-
warnings.warn(
|
457
|
-
"Playwright is not installed. Install with: pip install fastled[full]. "
|
458
|
-
"Falling back to default browser."
|
459
|
-
)
|
460
|
-
# Fall back to default browser
|
461
|
-
import webbrowser
|
462
|
-
|
463
|
-
webbrowser.open(url)
|
464
|
-
return
|
465
493
|
|
466
494
|
try:
|
467
495
|
# Run Playwright in a separate process to avoid blocking
|
@@ -469,7 +497,7 @@ class PlaywrightBrowserProxy:
|
|
469
497
|
|
470
498
|
self.process = multiprocessing.Process(
|
471
499
|
target=run_playwright_browser_persistent,
|
472
|
-
args=(url, headless),
|
500
|
+
args=(url, headless, enable_extensions),
|
473
501
|
)
|
474
502
|
self.process.start()
|
475
503
|
|
@@ -531,20 +559,23 @@ class PlaywrightBrowserProxy:
|
|
531
559
|
self.process = None
|
532
560
|
|
533
561
|
|
534
|
-
def run_playwright_browser_persistent(
|
562
|
+
def run_playwright_browser_persistent(
|
563
|
+
url: str, headless: bool = False, enable_extensions: bool = True
|
564
|
+
) -> None:
|
535
565
|
"""Run Playwright browser in a persistent mode that stays alive until terminated.
|
536
566
|
|
537
567
|
Args:
|
538
568
|
url: The URL to open
|
539
569
|
headless: Whether to run in headless mode
|
570
|
+
enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
|
540
571
|
"""
|
541
|
-
if not is_playwright_available():
|
542
|
-
return
|
543
572
|
|
544
573
|
async def main():
|
545
574
|
browser = None
|
546
575
|
try:
|
547
|
-
browser = PlaywrightBrowser(
|
576
|
+
browser = PlaywrightBrowser(
|
577
|
+
headless=headless, enable_extensions=enable_extensions
|
578
|
+
)
|
548
579
|
await browser.start()
|
549
580
|
await browser.open_url(url)
|
550
581
|
|
@@ -563,7 +594,9 @@ def run_playwright_browser_persistent(url: str, headless: bool = False) -> None:
|
|
563
594
|
if install_playwright_browsers():
|
564
595
|
print("🎭 Retrying browser startup...")
|
565
596
|
# Try again with fresh browser instance
|
566
|
-
browser = PlaywrightBrowser(
|
597
|
+
browser = PlaywrightBrowser(
|
598
|
+
headless=headless, enable_extensions=enable_extensions
|
599
|
+
)
|
567
600
|
await browser.start()
|
568
601
|
await browser.open_url(url)
|
569
602
|
|
@@ -596,7 +629,9 @@ def run_playwright_browser_persistent(url: str, headless: bool = False) -> None:
|
|
596
629
|
print(f"Playwright browser failed: {e}")
|
597
630
|
|
598
631
|
|
599
|
-
def open_with_playwright(
|
632
|
+
def open_with_playwright(
|
633
|
+
url: str, headless: bool = False, enable_extensions: bool = True
|
634
|
+
) -> PlaywrightBrowserProxy:
|
600
635
|
"""Open URL with Playwright browser and return a proxy object for lifecycle management.
|
601
636
|
|
602
637
|
This function can be used as a drop-in replacement for webbrowser.open().
|
@@ -604,12 +639,13 @@ def open_with_playwright(url: str, headless: bool = False) -> PlaywrightBrowserP
|
|
604
639
|
Args:
|
605
640
|
url: The URL to open
|
606
641
|
headless: Whether to run in headless mode
|
642
|
+
enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
|
607
643
|
|
608
644
|
Returns:
|
609
645
|
PlaywrightBrowserProxy object for managing the browser lifecycle
|
610
646
|
"""
|
611
647
|
proxy = PlaywrightBrowserProxy()
|
612
|
-
proxy.open(url, headless)
|
648
|
+
proxy.open(url, headless, enable_extensions)
|
613
649
|
return proxy
|
614
650
|
|
615
651
|
|
@@ -621,8 +657,6 @@ def install_playwright_browsers() -> bool:
|
|
621
657
|
Returns:
|
622
658
|
True if installation was successful or browsers were already installed
|
623
659
|
"""
|
624
|
-
if not PLAYWRIGHT_AVAILABLE:
|
625
|
-
return False
|
626
660
|
|
627
661
|
try:
|
628
662
|
import os
|
@@ -669,6 +703,23 @@ def install_playwright_browsers() -> bool:
|
|
669
703
|
if result.returncode == 0:
|
670
704
|
print("✅ Playwright browsers installed successfully!")
|
671
705
|
print(f" Location: {playwright_dir}")
|
706
|
+
|
707
|
+
# Also download the C++ DevTools Support extension
|
708
|
+
try:
|
709
|
+
from fastled.chrome_extension_downloader import (
|
710
|
+
download_cpp_devtools_extension,
|
711
|
+
)
|
712
|
+
|
713
|
+
extension_path = download_cpp_devtools_extension()
|
714
|
+
if extension_path:
|
715
|
+
print(
|
716
|
+
"✅ C++ DevTools Support extension ready for DWARF debugging!"
|
717
|
+
)
|
718
|
+
else:
|
719
|
+
print("⚠️ C++ DevTools Support extension download failed")
|
720
|
+
except Exception as e:
|
721
|
+
print(f"⚠️ Failed to setup C++ DevTools Support extension: {e}")
|
722
|
+
|
672
723
|
return True
|
673
724
|
else:
|
674
725
|
print(
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: fastled
|
3
|
-
Version: 1.4.
|
3
|
+
Version: 1.4.9
|
4
4
|
Summary: FastLED Wasm Compiler
|
5
5
|
Home-page: https://github.com/zackees/fastled-wasm
|
6
6
|
Maintainer: Zachary Vorhies
|
@@ -22,8 +22,7 @@ Requires-Dist: Flask>=3.0.0
|
|
22
22
|
Requires-Dist: flask-cors>=4.0.0
|
23
23
|
Requires-Dist: livereload
|
24
24
|
Requires-Dist: disklru>=2.0.4
|
25
|
-
|
26
|
-
Requires-Dist: playwright>=1.40.0; extra == "full"
|
25
|
+
Requires-Dist: playwright>=1.40.0
|
27
26
|
Dynamic: home-page
|
28
27
|
Dynamic: license-file
|
29
28
|
Dynamic: maintainer
|
@@ -73,34 +72,28 @@ $ cd mysketchdirectory
|
|
73
72
|
$ fastled
|
74
73
|
```
|
75
74
|
|
76
|
-
##
|
75
|
+
## App-like Browser Support
|
77
76
|
|
78
|
-
|
77
|
+
FastLED includes Playwright for enhanced browser control and automation capabilities. To use the app-like browser experience, use the `--app` flag:
|
79
78
|
|
80
79
|
```bash
|
81
|
-
$
|
80
|
+
$ fastled --app my_sketch
|
82
81
|
```
|
83
82
|
|
84
|
-
|
85
|
-
|
86
|
-
```bash
|
87
|
-
$ fastled my_sketch
|
88
|
-
```
|
89
|
-
|
90
|
-
The Playwright browser provides better automation capabilities and is especially useful for:
|
83
|
+
The `--app` flag provides better automation capabilities and is especially useful for:
|
91
84
|
- Automated testing of your LED sketches
|
92
85
|
- Consistent cross-platform browser behavior
|
93
86
|
- Advanced debugging and development workflows
|
94
87
|
- Persistent browser sessions that stay open until the FastLED process exits
|
95
88
|
|
96
89
|
**Key Benefits:**
|
97
|
-
-
|
90
|
+
- Triggered by the `--app` flag - automatically downloads browsers if needed
|
98
91
|
- The Playwright browser remains open throughout your development session
|
99
92
|
- Automatic cleanup when the FastLED process exits
|
100
93
|
- Better control over browser behavior and automation capabilities
|
101
94
|
- Consistent behavior across different platforms
|
102
95
|
|
103
|
-
If
|
96
|
+
If you don't use the `--app` flag, the system will use your default browser.
|
104
97
|
|
105
98
|
# Install
|
106
99
|
|
@@ -110,26 +103,14 @@ This is a python app, so any python package manager will work. We also provide p
|
|
110
103
|
|
111
104
|
`pip install fastled`
|
112
105
|
|
113
|
-
### Pip with Playwright Support
|
114
|
-
|
115
|
-
`pip install fastled[full]`
|
116
|
-
|
117
106
|
### UV
|
118
107
|
|
119
108
|
`uv pip install fastled --system`
|
120
109
|
|
121
|
-
### UV with Playwright Support
|
122
|
-
|
123
|
-
`uv pip install "fastled[full]" --system`
|
124
|
-
|
125
110
|
### Pipx
|
126
111
|
|
127
112
|
`pipx install fastled`
|
128
113
|
|
129
|
-
### Pipx with Playwright Support
|
130
|
-
|
131
|
-
`pipx install "fastled[full]"`
|
132
|
-
|
133
114
|
### Executables
|
134
115
|
|
135
116
|
* Windows: https://github.com/zackees/fastled-wasm/releases/latest/download/fastled-windows-x64.zip
|
@@ -371,7 +352,7 @@ A: A big chunk of space is being used by unnecessary javascript `emscripten` bun
|
|
371
352
|
|
372
353
|
|
373
354
|
# Revisions
|
374
|
-
* 1.4.00 - Browser now uses playwright when `
|
355
|
+
* 1.4.00 - Browser now uses playwright when `--app` flag is used. Much better app like experience.
|
375
356
|
* 1.2.31 - Bunch of fixes and ease of use while compiling code in the repo.
|
376
357
|
* 1.2.22 - Prefer to use `live-server` from npm. If npm exists on the system then do a background install of `live-server` for next run.
|
377
358
|
* 1.2.20 - Fixed up path issue for web browser launch for hot reload.
|
@@ -1,12 +1,13 @@
|
|
1
|
-
fastled/__init__.py,sha256=
|
1
|
+
fastled/__init__.py,sha256=dahiY41HLLotTjqmpVJmXSwUEp8NKqoZ57jt55hBLa4,7667
|
2
2
|
fastled/__main__.py,sha256=OcKv2ER1_iQAsZzLIUb3C8hRC9L2clNOhCrjpshrlf4,336
|
3
|
-
fastled/__version__.py,sha256=
|
3
|
+
fastled/__version__.py,sha256=ZiZk3FJ43XnaDFc89u1d82Mxs7giHFYqSujWneamokY,372
|
4
4
|
fastled/app.py,sha256=6XOuObi72AUnZXASDOVbcSflr4He0xnIDk5P8nVmVus,6131
|
5
|
-
fastled/args.py,sha256=
|
5
|
+
fastled/args.py,sha256=d8Afa7NMcNLMIQqIBX_ZOL5JOyeJ7XCch4LdkhFNChk,3671
|
6
|
+
fastled/chrome_extension_downloader.py,sha256=48YyQrsuK1TVXPuAvRGzqkQJnx0991Ka6OVUo1A58zU,7079
|
6
7
|
fastled/cli.py,sha256=drgR2AOxVrj3QEz58iiKscYAumbbin2vIV-k91VCOAA,561
|
7
8
|
fastled/cli_test.py,sha256=W-1nODZrip_JU6BEbYhxOa4ckxduOsiX8zIoRkTyxv4,550
|
8
9
|
fastled/cli_test_interactive.py,sha256=BjNhveZOk5aCffHbcrxPQQjWmAuj4ClVKKcKX5eY6yM,542
|
9
|
-
fastled/client_server.py,sha256=
|
10
|
+
fastled/client_server.py,sha256=H590e_FkoWlj7DX_SD0hEkbtoWJoG73MrHtzxEFvqnA,21441
|
10
11
|
fastled/compile_server.py,sha256=iGUjteXKp5Dlp7mxAE4eD4s0NWgApRIp4ZjtcAN2iZY,3124
|
11
12
|
fastled/compile_server_impl.py,sha256=iCwNCs7YxypUuVPmY4979mOgoH9OiuAJa1a1bmpG1cc,12567
|
12
13
|
fastled/docker_manager.py,sha256=rkq39ZKrU6NHIyDa3mzs0Unb6o9oMeAwxhqiuHJU_RY,40291
|
@@ -14,11 +15,11 @@ fastled/filewatcher.py,sha256=gEcJJHTDJ1X3gKJzltmEBhixWGbZj2eJD7a4vwSvITQ,10036
|
|
14
15
|
fastled/find_good_connection.py,sha256=xnrJjrbwNZUkvSQRn_ZTMoVh5GBWTbO-lEsr_L95xq8,3372
|
15
16
|
fastled/keyboard.py,sha256=UTAsqCn1UMYnB8YDzENiLTj4GeL45tYfEcO7_5fLFEg,3556
|
16
17
|
fastled/keyz.py,sha256=LO-8m_7CpNDiZLM-FXhQ30f9gN1bUYz5lOsUPTIbI-c,4020
|
17
|
-
fastled/live_client.py,sha256=
|
18
|
-
fastled/open_browser.py,sha256=
|
19
|
-
fastled/parse_args.py,sha256=
|
18
|
+
fastled/live_client.py,sha256=yp_ujG92EHYpSedGOUteuG2nQvMKbp1GbUpgQ6nU4Dc,3083
|
19
|
+
fastled/open_browser.py,sha256=gtBF7hoc0qcmWM_om0crcJ26TsmX5sRTCulaVRQLjP8,5023
|
20
|
+
fastled/parse_args.py,sha256=UiGgFoR_7ZCVC26rxE4FpxpmPu9xBhiiCD4pwmY_gLw,11520
|
20
21
|
fastled/paths.py,sha256=VsPmgu0lNSCFOoEC0BsTYzDygXqy15AHUfN-tTuzDZA,99
|
21
|
-
fastled/playwright_browser.py,sha256=
|
22
|
+
fastled/playwright_browser.py,sha256=qsPQiwamSOSBuXPoqgr1ybIRZbjJH5MeoMNLjpR4aTg,26776
|
22
23
|
fastled/print_filter.py,sha256=nc_rqYYdCUPinFycaK7fiQF5PG1up51pmJptR__QyAs,1499
|
23
24
|
fastled/project_init.py,sha256=bBt4DwmW5hZkm9ICt9Qk-0Nr_0JQM7icCgH5Iv-bCQs,3984
|
24
25
|
fastled/select_sketch_directory.py,sha256=-eudwCns3AKj4HuHtSkZAFwbnf005SNL07pOzs9VxnE,1383
|
@@ -40,9 +41,9 @@ fastled/site/build.py,sha256=2YKU_UWKlJdGnjdbAbaL0co6kceFMSTVYwH1KCmgPZA,13987
|
|
40
41
|
fastled/site/examples.py,sha256=s6vj2zJc6BfKlnbwXr1QWY1mzuDBMt6j5MEBOWjO_U8,155
|
41
42
|
fastled/test/can_run_local_docker_tests.py,sha256=LEuUbHctRhNNFWcvnz2kEGmjDJeXO4c3kNpizm3yVJs,400
|
42
43
|
fastled/test/examples.py,sha256=GfaHeY1E8izBl6ZqDVjz--RHLyVR4NRnQ5pBesCFJFY,1673
|
43
|
-
fastled-1.4.
|
44
|
-
fastled-1.4.
|
45
|
-
fastled-1.4.
|
46
|
-
fastled-1.4.
|
47
|
-
fastled-1.4.
|
48
|
-
fastled-1.4.
|
44
|
+
fastled-1.4.9.dist-info/licenses/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
|
45
|
+
fastled-1.4.9.dist-info/METADATA,sha256=y9Q00P53Nh_2KnIKWDMuKBrcq5_JqAp8MlmYf0PvwEw,31909
|
46
|
+
fastled-1.4.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
47
|
+
fastled-1.4.9.dist-info/entry_points.txt,sha256=RCwmzCSOS4-C2i9EziANq7Z2Zb4KFnEMR1FQC0bBwAw,101
|
48
|
+
fastled-1.4.9.dist-info/top_level.txt,sha256=Bbv5kpJpZhWNCvDF4K0VcvtBSDMa8B7PTOrZa9CezHY,8
|
49
|
+
fastled-1.4.9.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|