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 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
- no_playwright: bool = False,
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
- no_playwright=no_playwright,
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.7"
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
- no_playwright: bool
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
- no_playwright=args.no_playwright,
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
- no_playwright: bool = False,
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
- no_playwright=no_playwright,
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
- no_playwright = bool(args.no_playwright)
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
- no_playwright=no_playwright,
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
- no_playwright: bool = False,
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
- if (
134
- PLAYWRIGHT_AVAILABLE
135
- and open_with_playwright is not None
136
- and not no_playwright
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(url)
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
- "--no-playwright",
124
+ "--app",
125
125
  action="store_true",
126
- help="Disable Playwright browser and use default system browser instead",
126
+ help="Use Playwright app-like browser experience (will download browsers if needed)",
127
127
  )
128
128
  parser.add_argument(
129
129
  "-u",
@@ -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 platform
105
+ import tkinter
97
106
 
98
- if platform.system() == "Windows":
99
- import winreg
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
- try:
102
- key = winreg.OpenKey(
103
- winreg.HKEY_CURRENT_USER, r"Control Panel\Desktop\WindowMetrics"
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] 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)"
130
118
  )
131
119
  return None
132
120
 
133
121
  except Exception as e:
134
- print(f"[PYTHON] DPI detection failed: {e}")
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
- if is_playwright_available():
141
- from playwright.async_api import async_playwright
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
- self.playwright = async_playwright()
144
- playwright = await self.playwright.start()
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 and self.browser is not None:
168
- # Detect system device scale factor for natural browser behavior
169
- device_scale_factor = self._detect_device_scale_factor()
170
-
171
- # Create browser context with detected or default device scale factor
172
- if device_scale_factor:
173
- context = await self.browser.new_context(
174
- device_scale_factor=device_scale_factor
175
- )
176
- print(
177
- f"[PYTHON] Created browser context with device scale factor: {device_scale_factor:.2f}"
178
- )
179
- else:
180
- context = await self.browser.new_context()
181
- print(
182
- "[PYTHON] Created browser context with default device scale factor"
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
- self.page = await context.new_page()
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 _get_window_info(self) -> dict[str, int] | None:
230
- """Get browser window dimensions information.
284
+ async def _track_browser_adjust_viewport(self) -> None:
285
+ """Track browser window changes and adjust viewport accordingly.
231
286
 
232
- Returns:
233
- 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.
234
290
  """
235
- if self.page is None:
236
- return None
291
+ last_viewport = None
292
+ consecutive_same_count = 0
293
+ max_consecutive_same = 5
237
294
 
238
- try:
239
- return await self.page.evaluate(
240
- """
241
- () => {
242
- return {
243
- outerWidth: window.outerWidth,
244
- outerHeight: window.outerHeight,
245
- innerWidth: window.innerWidth,
246
- innerHeight: window.innerHeight,
247
- contentWidth: document.documentElement.clientWidth,
248
- contentHeight: document.documentElement.clientHeight
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
- print(
262
- "[PYTHON] Starting browser window tracking (outer size → viewport adjustment)"
263
- )
264
- 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
265
316
 
266
- while True:
267
- try:
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
- # Print current state occasionally
287
- if last_outer_size is None or current_outer != last_outer_size:
288
- print(
289
- 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
+ """
290
332
  )
291
333
 
292
- # Track changes in OUTER window size (user resizes browser)
293
- if last_outer_size is None or current_outer != last_outer_size:
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
- # Query the actual window dimensions after the viewport change
322
- updated_window_info = await self._get_window_info()
338
+ except Exception:
339
+ pass
323
340
 
324
- 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
325
347
 
326
- # Update our tracking with the actual final outer size
327
- last_outer_size = (
328
- updated_window_info["outerWidth"],
329
- updated_window_info["outerHeight"],
330
- )
331
- print(
332
- f"[PYTHON] Updated last_outer_size to actual final size: {last_outer_size}"
333
- )
334
- else:
335
- 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
+ )
336
362
 
337
- except Exception as e:
338
- 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")
339
367
 
340
- else:
341
- print("[PYTHON] Could not get browser window info")
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.browser is None:
350
- return
351
-
352
- try:
353
- # Wait for the browser to be closed
354
- while not self.browser.is_closed():
355
- await asyncio.sleep(1)
356
- except Exception:
357
- 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
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(url: str, headless: bool = False) -> None:
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(headless=headless)
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(headless=headless)
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(self, url: str, headless: bool = False) -> None:
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(url: str, headless: bool = False) -> None:
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(headless=headless)
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(headless=headless)
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(url: str, headless: bool = False) -> PlaywrightBrowserProxy:
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.7
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
- Provides-Extra: full
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
- ## Playwright Browser Support
75
+ ## App-like Browser Support
77
76
 
78
- For enhanced browser control and automation capabilities, you can install the full version with Playwright support:
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
- $ pip install fastled[full]
80
+ $ fastled --app my_sketch
82
81
  ```
83
82
 
84
- When installed, FastLED will automatically use Playwright to open your compiled sketch in a controlled browser environment instead of your system's default browser:
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
- - Automatically enabled when `fastled[full]` is installed - no additional flags needed
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 Playwright is not installed, the system will gracefully fall back to your default browser.
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 `pip install fastled[full]` is used. Much better app like experience.
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=l4uDkh_YOd24okNfn6eWjtTYaZ0woeYC7khv-vHMmTM,7775
1
+ fastled/__init__.py,sha256=dahiY41HLLotTjqmpVJmXSwUEp8NKqoZ57jt55hBLa4,7667
2
2
  fastled/__main__.py,sha256=OcKv2ER1_iQAsZzLIUb3C8hRC9L2clNOhCrjpshrlf4,336
3
- fastled/__version__.py,sha256=IIHIBgMnSIjry_2fDsUJNCOPtgR7hfhktRPPy1zDVjo,372
3
+ fastled/__version__.py,sha256=ZiZk3FJ43XnaDFc89u1d82Mxs7giHFYqSujWneamokY,372
4
4
  fastled/app.py,sha256=6XOuObi72AUnZXASDOVbcSflr4He0xnIDk5P8nVmVus,6131
5
- fastled/args.py,sha256=uCMyRIYM8gFE52O12YKUfA-rwJL8Zxwk_hsH3cusSac,3669
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=mxRgVtUwsFHGMkCY-rIWUQqiKY6ovUtrWApkhrobrfc,21662
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=aDZqSWDMpqNaEsT3u1nrBcdeIOItv-L0Gk2A10difLA,3209
18
- fastled/open_browser.py,sha256=mwjm65p2ydwmsaar7ooH4mhT5_qH_LZvXUpkRPPJ9eA,4881
19
- fastled/parse_args.py,sha256=htjap9tWZDJXnJ5upDwcy8EhecJD1uLZwacHR_T5ySs,11518
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=wa_MCrF_JqTDK635OCoL73Fwu2jbjsOZx5XNT_aKVbg,24379
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.7.dist-info/licenses/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
44
- fastled-1.4.7.dist-info/METADATA,sha256=djjNFRmkP71_6fnaTspzQ3Txpb2I35VjjRjPW20yt6c,32369
45
- fastled-1.4.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
46
- fastled-1.4.7.dist-info/entry_points.txt,sha256=RCwmzCSOS4-C2i9EziANq7Z2Zb4KFnEMR1FQC0bBwAw,101
47
- fastled-1.4.7.dist-info/top_level.txt,sha256=Bbv5kpJpZhWNCvDF4K0VcvtBSDMa8B7PTOrZa9CezHY,8
48
- fastled-1.4.7.dist-info/RECORD,,
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,,