fastled 1.4.8__py3-none-any.whl → 1.4.10__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 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.8"
4
+ __version__ = "1.4.10"
5
5
 
6
6
  __version_url_latest__ = "https://raw.githubusercontent.com/zackees/fastled-wasm/refs/heads/main/src/fastled/__version__.py"
@@ -0,0 +1,148 @@
1
+ """
2
+ Interruptible HTTP requests that can be cancelled with Ctrl+C.
3
+
4
+ This module provides cross-platform HTTP request functionality that can be
5
+ interrupted with Ctrl+C by using asyncio cancellation and periodic checks.
6
+ """
7
+
8
+ import asyncio
9
+
10
+ import httpx
11
+
12
+
13
+ class InterruptibleHTTPRequest:
14
+ """A wrapper for making HTTP requests that can be interrupted by Ctrl+C."""
15
+
16
+ def __init__(self):
17
+ self.cancelled = False
18
+
19
+ async def _make_request_async(
20
+ self,
21
+ url: str,
22
+ files: dict,
23
+ headers: dict,
24
+ transport: httpx.HTTPTransport | None = None,
25
+ timeout: float = 240,
26
+ follow_redirects: bool = True,
27
+ ) -> httpx.Response:
28
+ """Make an async HTTP request."""
29
+ # Convert sync transport to async transport if provided
30
+ async_transport = None
31
+ if transport is not None:
32
+ # For IPv4 connections, create async transport with local address
33
+ async_transport = httpx.AsyncHTTPTransport(local_address="0.0.0.0")
34
+
35
+ async with httpx.AsyncClient(
36
+ transport=async_transport,
37
+ timeout=timeout,
38
+ ) as client:
39
+ response = await client.post(
40
+ url,
41
+ follow_redirects=follow_redirects,
42
+ files=files,
43
+ headers=headers,
44
+ )
45
+ return response
46
+
47
+ def make_request_interruptible(
48
+ self,
49
+ url: str,
50
+ files: dict,
51
+ headers: dict,
52
+ transport: httpx.HTTPTransport | None = None,
53
+ timeout: float = 240,
54
+ follow_redirects: bool = True,
55
+ ) -> httpx.Response:
56
+ """Make an HTTP request that can be interrupted by Ctrl+C."""
57
+ try:
58
+ # Create a new event loop if we're not in one
59
+ try:
60
+ loop = asyncio.get_running_loop()
61
+ # We're already in an event loop, use run_in_executor
62
+ return asyncio.run_coroutine_threadsafe(
63
+ self._run_with_keyboard_check(
64
+ url, files, headers, transport, timeout, follow_redirects
65
+ ),
66
+ loop,
67
+ ).result()
68
+ except RuntimeError:
69
+ # No running loop, create one
70
+ return asyncio.run(
71
+ self._run_with_keyboard_check(
72
+ url, files, headers, transport, timeout, follow_redirects
73
+ )
74
+ )
75
+ except KeyboardInterrupt:
76
+ print("\nHTTP request cancelled by user")
77
+ raise
78
+
79
+ async def _run_with_keyboard_check(
80
+ self,
81
+ url: str,
82
+ files: dict,
83
+ headers: dict,
84
+ transport: httpx.HTTPTransport | None = None,
85
+ timeout: float = 240,
86
+ follow_redirects: bool = True,
87
+ ) -> httpx.Response:
88
+ """Run the request with periodic keyboard interrupt checks."""
89
+ task = asyncio.create_task(
90
+ self._make_request_async(
91
+ url, files, headers, transport, timeout, follow_redirects
92
+ )
93
+ )
94
+
95
+ # Poll for keyboard interrupt while waiting for the request
96
+ # This approach allows the task to be cancelled when KeyboardInterrupt
97
+ # is raised in the calling thread
98
+ while not task.done():
99
+ try:
100
+ # Wait for either completion or a short timeout
101
+ response = await asyncio.wait_for(asyncio.shield(task), timeout=0.1)
102
+ return response
103
+ except asyncio.TimeoutError:
104
+ # Continue waiting - the short timeout allows for more responsive
105
+ # cancellation when KeyboardInterrupt is raised
106
+ continue
107
+ except KeyboardInterrupt:
108
+ task.cancel()
109
+ print("\nHTTP request cancelled by user")
110
+ raise
111
+
112
+ return await task
113
+
114
+
115
+ def make_interruptible_post_request(
116
+ url: str,
117
+ files: dict | None = None,
118
+ headers: dict | None = None,
119
+ transport: httpx.HTTPTransport | None = None,
120
+ timeout: float = 240,
121
+ follow_redirects: bool = True,
122
+ ) -> httpx.Response:
123
+ """
124
+ Convenience function to make an interruptible POST request.
125
+
126
+ Args:
127
+ url: The URL to make the request to
128
+ files: Files to upload (optional)
129
+ headers: HTTP headers (optional)
130
+ transport: HTTP transport to use (optional)
131
+ timeout: Request timeout in seconds
132
+ follow_redirects: Whether to follow redirects
133
+
134
+ Returns:
135
+ The HTTP response
136
+
137
+ Raises:
138
+ KeyboardInterrupt: If the request was cancelled by Ctrl+C
139
+ """
140
+ request_handler = InterruptibleHTTPRequest()
141
+ return request_handler.make_request_interruptible(
142
+ url=url,
143
+ files=files or {},
144
+ headers=headers or {},
145
+ transport=transport,
146
+ timeout=timeout,
147
+ follow_redirects=follow_redirects,
148
+ )
fastled/open_browser.py CHANGED
@@ -6,7 +6,7 @@ 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
+ from fastled.playwright.playwright_browser import open_with_playwright
10
10
  from fastled.server_flask import run_flask_in_thread
11
11
 
12
12
  # Global reference to keep Playwright browser alive
@@ -128,7 +128,9 @@ def spawn_http_server(
128
128
  if should_use_playwright:
129
129
  if app:
130
130
  # For --app mode, try to install browsers if needed
131
- from fastled.playwright_browser import install_playwright_browsers
131
+ from fastled.playwright.playwright_browser import (
132
+ install_playwright_browsers,
133
+ )
132
134
 
133
135
  install_playwright_browsers()
134
136
 
@@ -136,8 +138,13 @@ def spawn_http_server(
136
138
  print(
137
139
  "Auto-resize enabled: Browser window will automatically adjust to content size"
138
140
  )
141
+ print(
142
+ "🔧 C++ DevTools Support extension will be loaded for DWARF debugging"
143
+ )
139
144
  global _playwright_browser_proxy
140
- _playwright_browser_proxy = open_with_playwright(url)
145
+ _playwright_browser_proxy = open_with_playwright(
146
+ url, enable_extensions=True
147
+ )
141
148
  else:
142
149
  print(f"Opening browser to {url}")
143
150
  import webbrowser
@@ -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")
@@ -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.playwright.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 platform
105
+ import tkinter
78
106
 
79
- if platform.system() == "Windows":
80
- import winreg
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
- try:
83
- key = winreg.OpenKey(
84
- winreg.HKEY_CURRENT_USER, r"Control Panel\Desktop\WindowMetrics"
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] Device scale detection not implemented for {platform.system()}"
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] DPI detection failed: {e}")
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
- # Get custom Chromium executable path if available
128
- executable_path = get_chromium_executable_path()
129
- launch_kwargs = {
130
- "headless": self.headless,
131
- "args": [
132
- "--disable-dev-shm-usage",
133
- "--disable-web-security",
134
- "--allow-running-insecure-content",
135
- ],
136
- }
137
-
138
- if executable_path:
139
- launch_kwargs["executable_path"] = executable_path
140
- print(f"[PYTHON] Using custom Chromium executable: {executable_path}")
141
-
142
- self.browser = await playwright.chromium.launch(**launch_kwargs)
143
-
144
- if self.page is None and self.browser is not None:
145
- # Detect system device scale factor for natural browser behavior
146
- device_scale_factor = self._detect_device_scale_factor()
147
-
148
- # Create browser context with detected or default device scale factor
149
- if device_scale_factor:
150
- context = await self.browser.new_context(
151
- device_scale_factor=device_scale_factor
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
- f"[PYTHON] Created browser context with device scale factor: {device_scale_factor:.2f}"
164
+ "[PYTHON] Started Playwright browser with C++ DevTools Support extension"
155
165
  )
166
+
156
167
  else:
157
- context = await self.browser.new_context()
158
- print(
159
- "[PYTHON] Created browser context with default device scale factor"
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
- self.page = await context.new_page()
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 _get_window_info(self) -> dict[str, int] | None:
207
- """Get browser window dimensions information.
284
+ async def _track_browser_adjust_viewport(self) -> None:
285
+ """Track browser window changes and adjust viewport accordingly.
208
286
 
209
- Returns:
210
- Dictionary containing window dimensions or None if unable to retrieve
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
- if self.page is None:
213
- return None
291
+ last_viewport = None
292
+ consecutive_same_count = 0
293
+ max_consecutive_same = 5
214
294
 
215
- try:
216
- return await self.page.evaluate(
217
- """
218
- () => {
219
- return {
220
- outerWidth: window.outerWidth,
221
- outerHeight: window.outerHeight,
222
- innerWidth: window.innerWidth,
223
- innerHeight: window.innerHeight,
224
- contentWidth: document.documentElement.clientWidth,
225
- contentHeight: document.documentElement.clientHeight
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
- print(
239
- "[PYTHON] Starting browser window tracking (outer size → viewport adjustment)"
240
- )
241
- last_outer_size = None
312
+ # Check if viewport changed
313
+ if current_viewport != last_viewport:
314
+ last_viewport = current_viewport
315
+ consecutive_same_count = 0
242
316
 
243
- while True:
244
- try:
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
- # Print current state occasionally
264
- if last_outer_size is None or current_outer != last_outer_size:
265
- print(
266
- f"[PYTHON] Browser: outer={window_info['outerWidth']}x{window_info['outerHeight']}, content={window_info['contentWidth']}x{window_info['contentHeight']}"
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
- # Track changes in OUTER window size (user resizes browser)
270
- if last_outer_size is None or current_outer != last_outer_size:
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
- # Query the actual window dimensions after the viewport change
299
- updated_window_info = await self._get_window_info()
338
+ except Exception:
339
+ pass
300
340
 
301
- if updated_window_info:
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
- # Update our tracking with the actual final outer size
304
- last_outer_size = (
305
- updated_window_info["outerWidth"],
306
- updated_window_info["outerHeight"],
307
- )
308
- print(
309
- f"[PYTHON] Updated last_outer_size to actual final size: {last_outer_size}"
310
- )
311
- else:
312
- print("[PYTHON] Could not get updated window info")
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
- except Exception as e:
315
- print(f"[PYTHON] Failed to set viewport: {e}")
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
- else:
318
- print("[PYTHON] Could not get browser window info")
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.browser is None:
327
- return
328
-
329
- try:
330
- # Wait for the browser to be closed
331
- while not self.browser.is_closed():
332
- await asyncio.sleep(1)
333
- except Exception:
334
- pass
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(url: str, headless: bool = False) -> None:
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(headless=headless)
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(headless=headless)
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(self, url: str, headless: bool = False) -> None:
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(url: str, headless: bool = False) -> None:
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(headless=headless)
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(headless=headless)
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(url: str, headless: bool = False) -> PlaywrightBrowserProxy:
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.playwright.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(
fastled/web_compile.py CHANGED
@@ -9,13 +9,14 @@ from pathlib import Path
9
9
  import httpx
10
10
 
11
11
  from fastled.find_good_connection import find_good_connection
12
+ from fastled.interruptible_http import make_interruptible_post_request
12
13
  from fastled.settings import SERVER_PORT
13
14
  from fastled.types import BuildMode, CompileResult
14
15
  from fastled.zip_files import ZipResult, zip_files
15
16
 
16
17
  DEFAULT_HOST = "https://fastled.onrender.com"
17
18
  ENDPOINT_COMPILED_WASM = "compile/wasm"
18
- _TIMEOUT = 60 * 4 # 2 mins timeout
19
+ _TIMEOUT = 60 * 4 # 4 mins timeout
19
20
  _AUTH_TOKEN = "oBOT5jbsO4ztgrpNsQwlmFLIKB"
20
21
 
21
22
 
@@ -83,25 +84,26 @@ def _compile_libfastled(
83
84
  httpx.HTTPTransport(local_address="0.0.0.0") if connection_result.ipv4 else None
84
85
  )
85
86
 
86
- with httpx.Client(
87
+ headers = {
88
+ "accept": "application/json",
89
+ "authorization": auth_token,
90
+ "build": build_mode.value.lower(),
91
+ }
92
+
93
+ url = f"{connection_result.host}/compile/libfastled"
94
+ print(f"Compiling libfastled on {url} via {ipv4_stmt}")
95
+
96
+ # Use interruptible HTTP request
97
+ response = make_interruptible_post_request(
98
+ url=url,
99
+ files={}, # No files for libfastled compilation
100
+ headers=headers,
87
101
  transport=transport,
88
102
  timeout=_TIMEOUT * 2, # Give more time for library compilation
89
- ) as client:
90
- headers = {
91
- "accept": "application/json",
92
- "authorization": auth_token,
93
- "build": build_mode.value.lower(),
94
- }
95
-
96
- url = f"{connection_result.host}/compile/libfastled"
97
- print(f"Compiling libfastled on {url} via {ipv4_stmt}")
98
- response = client.post(
99
- url,
100
- headers=headers,
101
- timeout=_TIMEOUT * 2,
102
- )
103
+ follow_redirects=False,
104
+ )
103
105
 
104
- return response
106
+ return response
105
107
 
106
108
 
107
109
  def _send_compile_request(
@@ -131,37 +133,32 @@ def _send_compile_request(
131
133
 
132
134
  archive_size = len(zip_bytes)
133
135
 
134
- with httpx.Client(
136
+ headers = {
137
+ "accept": "application/json",
138
+ "authorization": auth_token,
139
+ "build": (
140
+ build_mode.value.lower() if build_mode else BuildMode.QUICK.value.lower()
141
+ ),
142
+ "profile": "true" if profile else "false",
143
+ "no-platformio": "true" if no_platformio else "false",
144
+ "allow-libcompile": "false", # Always false since we handle it manually
145
+ }
146
+
147
+ url = f"{connection_result.host}/{ENDPOINT_COMPILED_WASM}"
148
+ print(f"Compiling sketch on {url} via {ipv4_stmt}. Zip size: {archive_size} bytes")
149
+ files = {"file": ("wasm.zip", zip_bytes, "application/x-zip-compressed")}
150
+
151
+ # Use interruptible HTTP request
152
+ response = make_interruptible_post_request(
153
+ url=url,
154
+ files=files,
155
+ headers=headers,
135
156
  transport=transport,
136
157
  timeout=_TIMEOUT,
137
- ) as client:
138
- headers = {
139
- "accept": "application/json",
140
- "authorization": auth_token,
141
- "build": (
142
- build_mode.value.lower()
143
- if build_mode
144
- else BuildMode.QUICK.value.lower()
145
- ),
146
- "profile": "true" if profile else "false",
147
- "no-platformio": "true" if no_platformio else "false",
148
- "allow-libcompile": "false", # Always false since we handle it manually
149
- }
150
-
151
- url = f"{connection_result.host}/{ENDPOINT_COMPILED_WASM}"
152
- print(
153
- f"Compiling sketch on {url} via {ipv4_stmt}. Zip size: {archive_size} bytes"
154
- )
155
- files = {"file": ("wasm.zip", zip_bytes, "application/x-zip-compressed")}
156
- response = client.post(
157
- url,
158
- follow_redirects=True,
159
- files=files,
160
- headers=headers,
161
- timeout=_TIMEOUT,
162
- )
158
+ follow_redirects=True,
159
+ )
163
160
 
164
- return response
161
+ return response
165
162
 
166
163
 
167
164
  def _process_compile_response(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastled
3
- Version: 1.4.8
3
+ Version: 1.4.10
4
4
  Summary: FastLED Wasm Compiler
5
5
  Home-page: https://github.com/zackees/fastled-wasm
6
6
  Maintainer: Zachary Vorhies
@@ -1,6 +1,6 @@
1
1
  fastled/__init__.py,sha256=dahiY41HLLotTjqmpVJmXSwUEp8NKqoZ57jt55hBLa4,7667
2
2
  fastled/__main__.py,sha256=OcKv2ER1_iQAsZzLIUb3C8hRC9L2clNOhCrjpshrlf4,336
3
- fastled/__version__.py,sha256=_2ywp7KhqGJZb6YoOQ07zDtMr2v_9j2-5miU6u4w6_c,372
3
+ fastled/__version__.py,sha256=79y56vY5gQySq33JTd8MFfXtNmylXNSfvohTya9mP_M,373
4
4
  fastled/app.py,sha256=6XOuObi72AUnZXASDOVbcSflr4He0xnIDk5P8nVmVus,6131
5
5
  fastled/args.py,sha256=d8Afa7NMcNLMIQqIBX_ZOL5JOyeJ7XCch4LdkhFNChk,3671
6
6
  fastled/cli.py,sha256=drgR2AOxVrj3QEz58iiKscYAumbbin2vIV-k91VCOAA,561
@@ -12,13 +12,13 @@ fastled/compile_server_impl.py,sha256=iCwNCs7YxypUuVPmY4979mOgoH9OiuAJa1a1bmpG1c
12
12
  fastled/docker_manager.py,sha256=rkq39ZKrU6NHIyDa3mzs0Unb6o9oMeAwxhqiuHJU_RY,40291
13
13
  fastled/filewatcher.py,sha256=gEcJJHTDJ1X3gKJzltmEBhixWGbZj2eJD7a4vwSvITQ,10036
14
14
  fastled/find_good_connection.py,sha256=xnrJjrbwNZUkvSQRn_ZTMoVh5GBWTbO-lEsr_L95xq8,3372
15
+ fastled/interruptible_http.py,sha256=2QwUsRNJ1qawf_-Lp1l0dBady3TK0SrBFhmnWgM7oqg,4888
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=xyUQmLL0D05AMyc3TIYUtFFLCeO7uBra8HeTnxAjcCQ,4847
19
+ fastled/open_browser.py,sha256=N0d_D87sSBOUbBUl7gktJufdyM1laEKIERCXnEYCyu4,5086
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=2hHcVLSr3QkV3C-CWjmWXWKNDzychKbuQRHSXJA55Ms,22895
22
22
  fastled/print_filter.py,sha256=nc_rqYYdCUPinFycaK7fiQF5PG1up51pmJptR__QyAs,1499
23
23
  fastled/project_init.py,sha256=bBt4DwmW5hZkm9ICt9Qk-0Nr_0JQM7icCgH5Iv-bCQs,3984
24
24
  fastled/select_sketch_directory.py,sha256=-eudwCns3AKj4HuHtSkZAFwbnf005SNL07pOzs9VxnE,1383
@@ -31,18 +31,20 @@ fastled/string_diff.py,sha256=oTncu0qYdLlLUtYLLDB4bzdQ2OfzegAR6XNAzwE9fIs,6002
31
31
  fastled/types.py,sha256=ZDf1TbTT4XgA_pKIwr4JbkDB38_29ogSdDORjoT-zuY,1803
32
32
  fastled/util.py,sha256=TjhXbUNh4p2BGhNAldSeL68B7BBOjsWAXji5gy-vDEQ,1440
33
33
  fastled/version.py,sha256=TpBMiEVdO3_sUZEu6wmwN8Q4AgX2BiCxStCsnPKh6E0,1209
34
- fastled/web_compile.py,sha256=Ql2DBRInZy7dOr1WZiUlhdg1ZVuU1nkbndRWiq7iENQ,10002
34
+ fastled/web_compile.py,sha256=8wJ6EK01hHsQLfeon4_TQaEDQZjsHHPidPPIzjMYUuM,9960
35
35
  fastled/zip_files.py,sha256=BgHFjaLJ7wF6mnzjqOgn76VcKDwhwc_-w_qyUG_-aNs,2815
36
36
  fastled/assets/example.txt,sha256=lTBovRjiz0_TgtAtbA1C5hNi2ffbqnNPqkKg6UiKCT8,54
37
37
  fastled/assets/localhost-key.pem,sha256=Q-CNO_UoOd8fFNN4ljcnqwUeCMhzTplRjLO2x0pYRlU,1704
38
38
  fastled/assets/localhost.pem,sha256=QTwUtTwjYWbm9m3pHW2IlK2nFZJ8b0pppxPjhgVZqQo,1619
39
+ fastled/playwright/chrome_extension_downloader.py,sha256=48YyQrsuK1TVXPuAvRGzqkQJnx0991Ka6OVUo1A58zU,7079
40
+ fastled/playwright/playwright_browser.py,sha256=5cx80kEV72CPlBT9fIcJNc9z40EFSOfXchBKBE9R4sE,26798
39
41
  fastled/site/build.py,sha256=2YKU_UWKlJdGnjdbAbaL0co6kceFMSTVYwH1KCmgPZA,13987
40
42
  fastled/site/examples.py,sha256=s6vj2zJc6BfKlnbwXr1QWY1mzuDBMt6j5MEBOWjO_U8,155
41
43
  fastled/test/can_run_local_docker_tests.py,sha256=LEuUbHctRhNNFWcvnz2kEGmjDJeXO4c3kNpizm3yVJs,400
42
44
  fastled/test/examples.py,sha256=GfaHeY1E8izBl6ZqDVjz--RHLyVR4NRnQ5pBesCFJFY,1673
43
- fastled-1.4.8.dist-info/licenses/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
44
- fastled-1.4.8.dist-info/METADATA,sha256=OtWn1uaXyCtBaTLs4HV7IVhMDcUWsxUYOboe50UXlXY,31909
45
- fastled-1.4.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
46
- fastled-1.4.8.dist-info/entry_points.txt,sha256=RCwmzCSOS4-C2i9EziANq7Z2Zb4KFnEMR1FQC0bBwAw,101
47
- fastled-1.4.8.dist-info/top_level.txt,sha256=Bbv5kpJpZhWNCvDF4K0VcvtBSDMa8B7PTOrZa9CezHY,8
48
- fastled-1.4.8.dist-info/RECORD,,
45
+ fastled-1.4.10.dist-info/licenses/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
46
+ fastled-1.4.10.dist-info/METADATA,sha256=ZUfEDRHSSsMmIZSZ4j3FCypY9lIZ4Yxuloo4HfU9QIc,31910
47
+ fastled-1.4.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
48
+ fastled-1.4.10.dist-info/entry_points.txt,sha256=RCwmzCSOS4-C2i9EziANq7Z2Zb4KFnEMR1FQC0bBwAw,101
49
+ fastled-1.4.10.dist-info/top_level.txt,sha256=Bbv5kpJpZhWNCvDF4K0VcvtBSDMa8B7PTOrZa9CezHY,8
50
+ fastled-1.4.10.dist-info/RECORD,,