fastled 1.4.8__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/__version__.py +1 -1
- fastled/chrome_extension_downloader.py +207 -0
- fastled/open_browser.py +6 -1
- fastled/playwright_browser.py +278 -181
- {fastled-1.4.8.dist-info → fastled-1.4.9.dist-info}/METADATA +1 -1
- {fastled-1.4.8.dist-info → fastled-1.4.9.dist-info}/RECORD +10 -9
- {fastled-1.4.8.dist-info → fastled-1.4.9.dist-info}/WHEEL +0 -0
- {fastled-1.4.8.dist-info → fastled-1.4.9.dist-info}/entry_points.txt +0 -0
- {fastled-1.4.8.dist-info → fastled-1.4.9.dist-info}/licenses/LICENSE +0 -0
- {fastled-1.4.8.dist-info → fastled-1.4.9.dist-info}/top_level.txt +0 -0
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"
|
@@ -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/open_browser.py
CHANGED
@@ -136,8 +136,13 @@ def spawn_http_server(
|
|
136
136
|
print(
|
137
137
|
"Auto-resize enabled: Browser window will automatically adjust to content size"
|
138
138
|
)
|
139
|
+
print(
|
140
|
+
"🔧 C++ DevTools Support extension will be loaded for DWARF debugging"
|
141
|
+
)
|
139
142
|
global _playwright_browser_proxy
|
140
|
-
_playwright_browser_proxy = open_with_playwright(
|
143
|
+
_playwright_browser_proxy = open_with_playwright(
|
144
|
+
url, enable_extensions=True
|
145
|
+
)
|
141
146
|
else:
|
142
147
|
print(f"Opening browser to {url}")
|
143
148
|
import webbrowser
|
fastled/playwright_browser.py
CHANGED
@@ -52,19 +52,47 @@ def get_chromium_executable_path() -> str | None:
|
|
52
52
|
class PlaywrightBrowser:
|
53
53
|
"""Playwright browser manager for FastLED sketches."""
|
54
54
|
|
55
|
-
def __init__(self, headless: bool = False):
|
55
|
+
def __init__(self, headless: bool = False, enable_extensions: bool = True):
|
56
56
|
"""Initialize the Playwright browser manager.
|
57
57
|
|
58
58
|
Args:
|
59
59
|
headless: Whether to run the browser in headless mode
|
60
|
+
enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
|
60
61
|
"""
|
61
62
|
|
62
63
|
self.headless = headless
|
64
|
+
self.enable_extensions = enable_extensions
|
63
65
|
self.auto_resize = True # Always enable auto-resize
|
64
66
|
self.browser: Any = None
|
67
|
+
self.context: Any = None
|
65
68
|
self.page: Any = None
|
66
69
|
self.playwright: Any = None
|
67
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
|
68
96
|
|
69
97
|
def _detect_device_scale_factor(self) -> float | None:
|
70
98
|
"""Detect the system's device scale factor for natural browser behavior.
|
@@ -74,92 +102,115 @@ class PlaywrightBrowser:
|
|
74
102
|
the value is outside reasonable bounds (0.5-4.0).
|
75
103
|
"""
|
76
104
|
try:
|
77
|
-
import
|
105
|
+
import tkinter
|
78
106
|
|
79
|
-
|
80
|
-
|
107
|
+
root = tkinter.Tk()
|
108
|
+
root.withdraw() # Hide the window
|
109
|
+
scale_factor = root.winfo_fpixels("1i") / 72.0
|
110
|
+
root.destroy()
|
81
111
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
)
|
86
|
-
dpi, _ = winreg.QueryValueEx(key, "AppliedDPI")
|
87
|
-
winreg.CloseKey(key)
|
88
|
-
device_scale_factor = dpi / 96.0
|
89
|
-
|
90
|
-
# Validate the scale factor is within reasonable bounds
|
91
|
-
if 0.5 <= device_scale_factor <= 4.0:
|
92
|
-
print(
|
93
|
-
f"[PYTHON] Detected Windows DPI scaling: {device_scale_factor:.2f}"
|
94
|
-
)
|
95
|
-
return device_scale_factor
|
96
|
-
else:
|
97
|
-
print(
|
98
|
-
f"[PYTHON] Detected scale factor {device_scale_factor:.2f} outside reasonable bounds"
|
99
|
-
)
|
100
|
-
return None
|
101
|
-
|
102
|
-
except (OSError, FileNotFoundError):
|
103
|
-
print(
|
104
|
-
"[PYTHON] Could not detect Windows DPI, using browser default"
|
105
|
-
)
|
106
|
-
return None
|
112
|
+
# Validate the scale factor is in a reasonable range
|
113
|
+
if 0.5 <= scale_factor <= 4.0:
|
114
|
+
return scale_factor
|
107
115
|
else:
|
108
|
-
# Future: Add support for other platforms (macOS, Linux) here
|
109
116
|
print(
|
110
|
-
f"[PYTHON]
|
117
|
+
f"[PYTHON] Detected scale factor {scale_factor:.2f} is outside reasonable bounds (0.5-4.0)"
|
111
118
|
)
|
112
119
|
return None
|
113
120
|
|
114
121
|
except Exception as e:
|
115
|
-
print(f"[PYTHON]
|
122
|
+
print(f"[PYTHON] Could not detect device scale factor: {e}")
|
116
123
|
return None
|
117
124
|
|
118
125
|
async def start(self) -> None:
|
119
126
|
"""Start the Playwright browser."""
|
120
|
-
if self.browser is None:
|
127
|
+
if self.browser is None and self.context is None:
|
121
128
|
|
122
129
|
from playwright.async_api import async_playwright
|
123
130
|
|
124
131
|
self.playwright = async_playwright()
|
125
132
|
playwright = await self.playwright.start()
|
126
133
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
"
|
134
|
-
"
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
+
}
|
150
|
+
|
151
|
+
# Get custom Chromium executable path if available
|
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
|
152
161
|
)
|
162
|
+
|
153
163
|
print(
|
154
|
-
|
164
|
+
"[PYTHON] Started Playwright browser with C++ DevTools Support extension"
|
155
165
|
)
|
166
|
+
|
156
167
|
else:
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
168
|
+
# Regular browser launch without extensions
|
169
|
+
executable_path = get_chromium_executable_path()
|
170
|
+
launch_kwargs = {
|
171
|
+
"headless": self.headless,
|
172
|
+
"args": [
|
173
|
+
"--disable-dev-shm-usage",
|
174
|
+
"--disable-web-security",
|
175
|
+
"--allow-running-insecure-content",
|
176
|
+
],
|
177
|
+
}
|
161
178
|
|
162
|
-
|
179
|
+
if executable_path:
|
180
|
+
launch_kwargs["executable_path"] = executable_path
|
181
|
+
print(
|
182
|
+
f"[PYTHON] Using custom Chromium executable: {executable_path}"
|
183
|
+
)
|
184
|
+
|
185
|
+
self.browser = await playwright.chromium.launch(**launch_kwargs)
|
186
|
+
|
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
|
+
)
|
212
|
+
|
213
|
+
self.page = await context.new_page()
|
163
214
|
|
164
215
|
async def open_url(self, url: str) -> None:
|
165
216
|
"""Open a URL in the Playwright browser.
|
@@ -190,6 +241,33 @@ class PlaywrightBrowser:
|
|
190
241
|
if self.auto_resize:
|
191
242
|
await self._setup_auto_resize()
|
192
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
|
+
|
193
271
|
async def _setup_auto_resize(self) -> None:
|
194
272
|
"""Set up automatic window resizing based on content size."""
|
195
273
|
if self.page is None:
|
@@ -203,119 +281,92 @@ class PlaywrightBrowser:
|
|
203
281
|
# Start polling loop that tracks browser window changes and adjusts viewport only
|
204
282
|
asyncio.create_task(self._track_browser_adjust_viewport())
|
205
283
|
|
206
|
-
async def
|
207
|
-
"""
|
284
|
+
async def _track_browser_adjust_viewport(self) -> None:
|
285
|
+
"""Track browser window changes and adjust viewport accordingly.
|
208
286
|
|
209
|
-
|
210
|
-
|
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.
|
211
290
|
"""
|
212
|
-
|
213
|
-
|
291
|
+
last_viewport = None
|
292
|
+
consecutive_same_count = 0
|
293
|
+
max_consecutive_same = 5
|
214
294
|
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
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
|
+
})
|
228
309
|
"""
|
229
|
-
|
230
|
-
except Exception:
|
231
|
-
return None
|
232
|
-
|
233
|
-
async def _track_browser_adjust_viewport(self) -> None:
|
234
|
-
"""Track browser window outer size changes and adjust viewport accordingly."""
|
235
|
-
if self.page is None:
|
236
|
-
return
|
310
|
+
)
|
237
311
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
312
|
+
# Check if viewport changed
|
313
|
+
if current_viewport != last_viewport:
|
314
|
+
last_viewport = current_viewport
|
315
|
+
consecutive_same_count = 0
|
242
316
|
|
243
|
-
|
244
|
-
|
245
|
-
# Wait 1 second between polls
|
246
|
-
await asyncio.sleep(1)
|
247
|
-
|
248
|
-
# Check if page is still alive
|
249
|
-
if self.page is None or self.page.is_closed():
|
250
|
-
print("[PYTHON] Page closed, signaling exit")
|
251
|
-
self._should_exit.set()
|
252
|
-
return
|
253
|
-
|
254
|
-
# Get browser window dimensions
|
255
|
-
window_info = await self._get_window_info()
|
256
|
-
|
257
|
-
if window_info:
|
258
|
-
current_outer = (
|
259
|
-
window_info["outerWidth"],
|
260
|
-
window_info["outerHeight"],
|
317
|
+
print(
|
318
|
+
f"[PYTHON] Viewport: {current_viewport['width']}x{current_viewport['height']}"
|
261
319
|
)
|
262
320
|
|
263
|
-
#
|
264
|
-
|
265
|
-
|
266
|
-
|
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
|
+
"""
|
267
332
|
)
|
268
333
|
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
if last_outer_size is not None:
|
273
|
-
print("[PYTHON] *** BROWSER WINDOW RESIZED ***")
|
274
|
-
print(
|
275
|
-
f"[PYTHON] Outer window changed from {last_outer_size[0]}x{last_outer_size[1]} to {current_outer[0]}x{current_outer[1]}"
|
276
|
-
)
|
277
|
-
|
278
|
-
last_outer_size = current_outer
|
279
|
-
|
280
|
-
# Set viewport to match the outer window size
|
281
|
-
if not self.headless:
|
282
|
-
try:
|
283
|
-
outer_width = int(window_info["outerWidth"])
|
284
|
-
outer_height = int(window_info["outerHeight"])
|
285
|
-
|
286
|
-
print(
|
287
|
-
f"[PYTHON] Setting viewport to match outer window size: {outer_width}x{outer_height}"
|
288
|
-
)
|
289
|
-
|
290
|
-
await self.page.set_viewport_size(
|
291
|
-
{"width": outer_width, "height": outer_height}
|
292
|
-
)
|
293
|
-
print("[PYTHON] Viewport set successfully")
|
294
|
-
|
295
|
-
# Wait briefly for browser to settle after viewport change
|
296
|
-
# 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
|
+
)
|
297
337
|
|
298
|
-
|
299
|
-
|
338
|
+
except Exception:
|
339
|
+
pass
|
300
340
|
|
301
|
-
|
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
|
302
347
|
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
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
|
+
)
|
313
362
|
|
314
|
-
|
315
|
-
|
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")
|
316
367
|
|
317
|
-
|
318
|
-
print("[PYTHON] Could not get browser
|
368
|
+
except Exception as e:
|
369
|
+
print(f"[PYTHON] Could not get browser info: {e}")
|
319
370
|
|
320
371
|
except Exception as e:
|
321
372
|
print(f"[PYTHON] Error in browser tracking: {e}")
|
@@ -323,15 +374,20 @@ class PlaywrightBrowser:
|
|
323
374
|
|
324
375
|
async def wait_for_close(self) -> None:
|
325
376
|
"""Wait for the browser to be closed."""
|
326
|
-
if self.
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
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
|
335
391
|
|
336
392
|
async def close(self) -> None:
|
337
393
|
"""Close the Playwright browser."""
|
@@ -339,6 +395,10 @@ class PlaywrightBrowser:
|
|
339
395
|
await self.page.close()
|
340
396
|
self.page = None
|
341
397
|
|
398
|
+
if self.context:
|
399
|
+
await self.context.close()
|
400
|
+
self.context = None
|
401
|
+
|
342
402
|
if self.browser:
|
343
403
|
await self.browser.close()
|
344
404
|
self.browser = None
|
@@ -348,18 +408,23 @@ class PlaywrightBrowser:
|
|
348
408
|
self.playwright = None
|
349
409
|
|
350
410
|
|
351
|
-
def run_playwright_browser(
|
411
|
+
def run_playwright_browser(
|
412
|
+
url: str, headless: bool = False, enable_extensions: bool = True
|
413
|
+
) -> None:
|
352
414
|
"""Run Playwright browser in a separate process.
|
353
415
|
|
354
416
|
Args:
|
355
417
|
url: The URL to open
|
356
418
|
headless: Whether to run in headless mode
|
419
|
+
enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
|
357
420
|
"""
|
358
421
|
|
359
422
|
async def main():
|
360
423
|
browser = None
|
361
424
|
try:
|
362
|
-
browser = PlaywrightBrowser(
|
425
|
+
browser = PlaywrightBrowser(
|
426
|
+
headless=headless, enable_extensions=enable_extensions
|
427
|
+
)
|
363
428
|
await browser.start()
|
364
429
|
await browser.open_url(url)
|
365
430
|
|
@@ -373,7 +438,9 @@ def run_playwright_browser(url: str, headless: bool = False) -> None:
|
|
373
438
|
if install_playwright_browsers():
|
374
439
|
print("🎭 Retrying browser startup...")
|
375
440
|
# Try again with fresh browser instance
|
376
|
-
browser = PlaywrightBrowser(
|
441
|
+
browser = PlaywrightBrowser(
|
442
|
+
headless=headless, enable_extensions=enable_extensions
|
443
|
+
)
|
377
444
|
await browser.start()
|
378
445
|
await browser.open_url(url)
|
379
446
|
|
@@ -413,12 +480,15 @@ class PlaywrightBrowserProxy:
|
|
413
480
|
self.monitor_thread = None
|
414
481
|
self._closing_intentionally = False
|
415
482
|
|
416
|
-
def open(
|
483
|
+
def open(
|
484
|
+
self, url: str, headless: bool = False, enable_extensions: bool = True
|
485
|
+
) -> None:
|
417
486
|
"""Open URL with Playwright browser and keep it alive.
|
418
487
|
|
419
488
|
Args:
|
420
489
|
url: The URL to open
|
421
490
|
headless: Whether to run in headless mode
|
491
|
+
enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
|
422
492
|
"""
|
423
493
|
|
424
494
|
try:
|
@@ -427,7 +497,7 @@ class PlaywrightBrowserProxy:
|
|
427
497
|
|
428
498
|
self.process = multiprocessing.Process(
|
429
499
|
target=run_playwright_browser_persistent,
|
430
|
-
args=(url, headless),
|
500
|
+
args=(url, headless, enable_extensions),
|
431
501
|
)
|
432
502
|
self.process.start()
|
433
503
|
|
@@ -489,18 +559,23 @@ class PlaywrightBrowserProxy:
|
|
489
559
|
self.process = None
|
490
560
|
|
491
561
|
|
492
|
-
def run_playwright_browser_persistent(
|
562
|
+
def run_playwright_browser_persistent(
|
563
|
+
url: str, headless: bool = False, enable_extensions: bool = True
|
564
|
+
) -> None:
|
493
565
|
"""Run Playwright browser in a persistent mode that stays alive until terminated.
|
494
566
|
|
495
567
|
Args:
|
496
568
|
url: The URL to open
|
497
569
|
headless: Whether to run in headless mode
|
570
|
+
enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
|
498
571
|
"""
|
499
572
|
|
500
573
|
async def main():
|
501
574
|
browser = None
|
502
575
|
try:
|
503
|
-
browser = PlaywrightBrowser(
|
576
|
+
browser = PlaywrightBrowser(
|
577
|
+
headless=headless, enable_extensions=enable_extensions
|
578
|
+
)
|
504
579
|
await browser.start()
|
505
580
|
await browser.open_url(url)
|
506
581
|
|
@@ -519,7 +594,9 @@ def run_playwright_browser_persistent(url: str, headless: bool = False) -> None:
|
|
519
594
|
if install_playwright_browsers():
|
520
595
|
print("🎭 Retrying browser startup...")
|
521
596
|
# Try again with fresh browser instance
|
522
|
-
browser = PlaywrightBrowser(
|
597
|
+
browser = PlaywrightBrowser(
|
598
|
+
headless=headless, enable_extensions=enable_extensions
|
599
|
+
)
|
523
600
|
await browser.start()
|
524
601
|
await browser.open_url(url)
|
525
602
|
|
@@ -552,7 +629,9 @@ def run_playwright_browser_persistent(url: str, headless: bool = False) -> None:
|
|
552
629
|
print(f"Playwright browser failed: {e}")
|
553
630
|
|
554
631
|
|
555
|
-
def open_with_playwright(
|
632
|
+
def open_with_playwright(
|
633
|
+
url: str, headless: bool = False, enable_extensions: bool = True
|
634
|
+
) -> PlaywrightBrowserProxy:
|
556
635
|
"""Open URL with Playwright browser and return a proxy object for lifecycle management.
|
557
636
|
|
558
637
|
This function can be used as a drop-in replacement for webbrowser.open().
|
@@ -560,12 +639,13 @@ def open_with_playwright(url: str, headless: bool = False) -> PlaywrightBrowserP
|
|
560
639
|
Args:
|
561
640
|
url: The URL to open
|
562
641
|
headless: Whether to run in headless mode
|
642
|
+
enable_extensions: Whether to enable Chrome extensions (C++ DevTools Support)
|
563
643
|
|
564
644
|
Returns:
|
565
645
|
PlaywrightBrowserProxy object for managing the browser lifecycle
|
566
646
|
"""
|
567
647
|
proxy = PlaywrightBrowserProxy()
|
568
|
-
proxy.open(url, headless)
|
648
|
+
proxy.open(url, headless, enable_extensions)
|
569
649
|
return proxy
|
570
650
|
|
571
651
|
|
@@ -623,6 +703,23 @@ def install_playwright_browsers() -> bool:
|
|
623
703
|
if result.returncode == 0:
|
624
704
|
print("✅ Playwright browsers installed successfully!")
|
625
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
|
+
|
626
723
|
return True
|
627
724
|
else:
|
628
725
|
print(
|
@@ -1,8 +1,9 @@
|
|
1
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
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
|
@@ -15,10 +16,10 @@ fastled/find_good_connection.py,sha256=xnrJjrbwNZUkvSQRn_ZTMoVh5GBWTbO-lEsr_L95x
|
|
15
16
|
fastled/keyboard.py,sha256=UTAsqCn1UMYnB8YDzENiLTj4GeL45tYfEcO7_5fLFEg,3556
|
16
17
|
fastled/keyz.py,sha256=LO-8m_7CpNDiZLM-FXhQ30f9gN1bUYz5lOsUPTIbI-c,4020
|
17
18
|
fastled/live_client.py,sha256=yp_ujG92EHYpSedGOUteuG2nQvMKbp1GbUpgQ6nU4Dc,3083
|
18
|
-
fastled/open_browser.py,sha256=
|
19
|
+
fastled/open_browser.py,sha256=gtBF7hoc0qcmWM_om0crcJ26TsmX5sRTCulaVRQLjP8,5023
|
19
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
|