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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. fastled/__init__.py +30 -2
  2. fastled/__main__.py +14 -0
  3. fastled/__version__.py +1 -1
  4. fastled/app.py +51 -2
  5. fastled/args.py +33 -0
  6. fastled/client_server.py +188 -40
  7. fastled/compile_server.py +10 -0
  8. fastled/compile_server_impl.py +34 -1
  9. fastled/docker_manager.py +56 -14
  10. fastled/emoji_util.py +27 -0
  11. fastled/filewatcher.py +6 -3
  12. fastled/find_good_connection.py +105 -0
  13. fastled/header_dump.py +63 -0
  14. fastled/install/__init__.py +1 -0
  15. fastled/install/examples_manager.py +62 -0
  16. fastled/install/extension_manager.py +113 -0
  17. fastled/install/main.py +156 -0
  18. fastled/install/project_detection.py +167 -0
  19. fastled/install/test_install.py +373 -0
  20. fastled/install/vscode_config.py +344 -0
  21. fastled/interruptible_http.py +148 -0
  22. fastled/live_client.py +21 -1
  23. fastled/open_browser.py +84 -16
  24. fastled/parse_args.py +110 -9
  25. fastled/playwright/chrome_extension_downloader.py +207 -0
  26. fastled/playwright/playwright_browser.py +773 -0
  27. fastled/playwright/resize_tracking.py +127 -0
  28. fastled/print_filter.py +52 -52
  29. fastled/project_init.py +20 -13
  30. fastled/select_sketch_directory.py +142 -19
  31. fastled/server_flask.py +37 -1
  32. fastled/settings.py +47 -3
  33. fastled/sketch.py +121 -4
  34. fastled/string_diff.py +162 -26
  35. fastled/test/examples.py +7 -5
  36. fastled/types.py +4 -0
  37. fastled/util.py +34 -0
  38. fastled/version.py +41 -41
  39. fastled/web_compile.py +379 -236
  40. fastled/zip_files.py +76 -0
  41. {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/METADATA +533 -508
  42. fastled-1.4.50.dist-info/RECORD +60 -0
  43. fastled-1.3.30.dist-info/RECORD +0 -44
  44. {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/WHEEL +0 -0
  45. {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/entry_points.txt +0 -0
  46. {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/licenses/LICENSE +0 -0
  47. {fastled-1.3.30.dist-info → fastled-1.4.50.dist-info}/top_level.txt +0 -0
fastled/open_browser.py CHANGED
@@ -6,8 +6,27 @@ import weakref
6
6
  from multiprocessing import Process
7
7
  from pathlib import Path
8
8
 
9
+ from fastled.playwright.playwright_browser import open_with_playwright
9
10
  from fastled.server_flask import run_flask_in_thread
10
11
 
12
+ # Global reference to keep Playwright browser alive
13
+ _playwright_browser_proxy = None
14
+
15
+
16
+ def cleanup_playwright_browser() -> None:
17
+ """Clean up the Playwright browser on exit."""
18
+ try:
19
+ global _playwright_browser_proxy
20
+ if _playwright_browser_proxy:
21
+ _playwright_browser_proxy.close()
22
+ _playwright_browser_proxy = None
23
+ except Exception:
24
+ pass
25
+
26
+
27
+ # Register cleanup function
28
+ atexit.register(cleanup_playwright_browser)
29
+
11
30
  DEFAULT_PORT = 8089 # different than live version.
12
31
  PYTHON_EXE = sys.executable
13
32
 
@@ -38,9 +57,15 @@ def is_port_free(port: int) -> bool:
38
57
  import httpx
39
58
 
40
59
  try:
41
- response = httpx.get(f"http://localhost:{port}", timeout=1)
42
- response.raise_for_status()
43
- return False
60
+ # Try HTTPS first, then fall back to HTTP
61
+ try:
62
+ response = httpx.get(f"https://localhost:{port}", timeout=1, verify=False)
63
+ response.raise_for_status()
64
+ return False
65
+ except (httpx.HTTPError, httpx.ConnectError):
66
+ response = httpx.get(f"http://localhost:{port}", timeout=1)
67
+ response.raise_for_status()
68
+ return False
44
69
  except (httpx.HTTPError, httpx.ConnectError):
45
70
  return True
46
71
 
@@ -56,16 +81,21 @@ def find_free_port(start_port: int) -> int:
56
81
  raise ValueError("Could not find a free port")
57
82
 
58
83
 
59
- def wait_for_server(port: int, timeout: int = 10) -> None:
84
+ def wait_for_server(port: int, timeout: int = 10, enable_https: bool = True) -> None:
60
85
  """Wait for the server to start."""
61
86
  from httpx import get
62
87
 
63
88
  future_time = time.time() + timeout
89
+ protocol = "https" if enable_https else "http"
64
90
  while future_time > time.time():
65
91
  try:
66
- url = f"http://localhost:{port}"
92
+ # Try the specified protocol (HTTPS with SSL verification disabled for self-signed certs)
93
+ url = f"{protocol}://localhost:{port}"
67
94
  # print(f"Waiting for server to start at {url}")
68
- response = get(url, timeout=1)
95
+ verify = (
96
+ False if enable_https else True
97
+ ) # Only disable SSL verification for HTTPS
98
+ response = get(url, timeout=1, verify=verify)
69
99
  if response.status_code == 200:
70
100
  return
71
101
  except Exception:
@@ -78,6 +108,8 @@ def spawn_http_server(
78
108
  compile_server_port: int,
79
109
  port: int | None = None,
80
110
  open_browser: bool = True,
111
+ app: bool = False,
112
+ enable_https: bool = True,
81
113
  ) -> Process:
82
114
 
83
115
  if port is not None and not is_port_free(port):
@@ -86,6 +118,17 @@ def spawn_http_server(
86
118
  offset = random.randint(0, 100)
87
119
  port = find_free_port(DEFAULT_PORT + offset)
88
120
 
121
+ # Get SSL certificate paths from the fastled assets directory if HTTPS is enabled
122
+ certfile: Path | None = None
123
+ keyfile: Path | None = None
124
+
125
+ if enable_https:
126
+ import fastled
127
+
128
+ assets_dir = Path(fastled.__file__).parent / "assets"
129
+ certfile = assets_dir / "localhost.pem"
130
+ keyfile = assets_dir / "localhost-key.pem"
131
+
89
132
  # port: int,
90
133
  # cwd: Path,
91
134
  # compile_server_port: int,
@@ -94,7 +137,7 @@ def spawn_http_server(
94
137
 
95
138
  proc = Process(
96
139
  target=run_flask_in_thread,
97
- args=(port, fastled_js, compile_server_port),
140
+ args=(port, fastled_js, compile_server_port, certfile, keyfile),
98
141
  daemon=True,
99
142
  )
100
143
  add_cleanup(proc)
@@ -103,16 +146,41 @@ def spawn_http_server(
103
146
  # Add to cleanup set with weak reference
104
147
  add_cleanup(proc)
105
148
 
106
- wait_for_server(port)
149
+ wait_for_server(port, enable_https=enable_https)
107
150
  if open_browser:
108
- print(f"Opening browser to http://localhost:{port}")
109
- import webbrowser
110
-
111
- webbrowser.open(
112
- url=f"http://localhost:{port}",
113
- new=1,
114
- autoraise=True,
115
- )
151
+ protocol = "https" if enable_https else "http"
152
+ url = f"{protocol}://localhost:{port}"
153
+ should_use_playwright = app
154
+
155
+ if should_use_playwright:
156
+ if app:
157
+ # For --app mode, try to install browsers if needed
158
+ from fastled.playwright.playwright_browser import (
159
+ install_playwright_browsers,
160
+ )
161
+
162
+ install_playwright_browsers()
163
+
164
+ print(f"Opening FastLED sketch in Playwright browser: {url}")
165
+ print(
166
+ "Auto-resize enabled: Browser window will automatically adjust to content size"
167
+ )
168
+ print(
169
+ "🔧 C++ DevTools Support extension will be loaded for DWARF debugging"
170
+ )
171
+ global _playwright_browser_proxy
172
+ _playwright_browser_proxy = open_with_playwright(
173
+ url, enable_extensions=True
174
+ )
175
+ else:
176
+ print(f"Opening browser to {url}")
177
+ import webbrowser
178
+
179
+ webbrowser.open(
180
+ url=url,
181
+ new=1,
182
+ autoraise=True,
183
+ )
116
184
  return proc
117
185
 
118
186
 
fastled/parse_args.py CHANGED
@@ -8,6 +8,7 @@ from fastled.project_init import project_init
8
8
  from fastled.select_sketch_directory import select_sketch_directory
9
9
  from fastled.settings import DEFAULT_URL, IMAGE_NAME
10
10
  from fastled.sketch import (
11
+ find_sketch_by_partial_name,
11
12
  find_sketch_directories,
12
13
  looks_like_fastled_repo,
13
14
  looks_like_sketch_directory,
@@ -30,16 +31,20 @@ FastLED WASM Compiler - Useful options:
30
31
  --init [example] Initialize one of the top tier WASM examples
31
32
  --web [url] Use web compiler
32
33
  --server Run the compiler server
34
+ --no-platformio Bypass PlatformIO constraints using local Docker compilation
33
35
  --quick Build in quick mode (default)
34
36
  --profile Enable profiling the C++ build system
35
37
  --update Update the docker image for the wasm compiler
38
+ --background-update Update the docker image in the background after compilation
36
39
  --purge Remove all FastLED containers and images
40
+ --emsdk-headers <path> Export EMSDK headers ZIP to specified path
37
41
  --version Show version information
38
42
  --help Show detailed help
39
43
  Examples:
40
44
  fastled (will auto detect the sketch directory and prompt you)
41
45
  fastled my_sketch
42
46
  fastled my_sketch --web (compiles using the web compiler only)
47
+ fastled my_sketch --background-update (compiles and updates docker image in background)
43
48
  fastled --init Blink (initializes a new sketch directory with the Blink example)
44
49
  fastled --server (runs the compiler server in the current directory)
45
50
 
@@ -81,8 +86,8 @@ def parse_args() -> Args:
81
86
  parser.add_argument(
82
87
  "--ram-disk-size",
83
88
  type=str,
84
- default="0",
85
- help="Set the size of the ramdisk for the docker container. Use suffixes like '25mb' or '1gb'.",
89
+ default="1gb",
90
+ help="Size of the RAM disk for compilation (e.g., '1gb', '512mb')",
86
91
  )
87
92
  parser.add_argument(
88
93
  "--web",
@@ -112,7 +117,17 @@ def parse_args() -> Args:
112
117
  parser.add_argument(
113
118
  "--no-auto-updates",
114
119
  action="store_true",
115
- help="Disable automatic updates of the wasm compiler image when using docker.",
120
+ help="Disable automatic updates of the wasm compiler image when using docker. (Default: False)",
121
+ )
122
+ parser.add_argument(
123
+ "--no-platformio",
124
+ action="store_true",
125
+ help="Bypass PlatformIO constraints by using local Docker compilation with custom build environment",
126
+ )
127
+ parser.add_argument(
128
+ "--app",
129
+ action="store_true",
130
+ help="Use Playwright app-like browser experience (will download browsers if needed)",
116
131
  )
117
132
  parser.add_argument(
118
133
  "-u",
@@ -121,6 +136,11 @@ def parse_args() -> Args:
121
136
  action="store_true",
122
137
  help="Update the wasm compiler (if necessary) before running",
123
138
  )
139
+ parser.add_argument(
140
+ "--background-update",
141
+ action="store_true",
142
+ help="Update the docker image in the background after compilation (user doesn't have to wait)",
143
+ )
124
144
  parser.add_argument(
125
145
  "--localhost",
126
146
  "--local",
@@ -152,6 +172,30 @@ def parse_args() -> Args:
152
172
  help="Remove all FastLED containers and images",
153
173
  )
154
174
 
175
+ parser.add_argument(
176
+ "--install",
177
+ action="store_true",
178
+ help="Install FastLED development environment with VSCode configuration and Auto Debug extension",
179
+ )
180
+
181
+ parser.add_argument(
182
+ "--dry-run",
183
+ action="store_true",
184
+ help="Run in dry-run mode (simulate actions without making changes)",
185
+ )
186
+
187
+ parser.add_argument(
188
+ "--no-interactive",
189
+ action="store_true",
190
+ help="Run in non-interactive mode (fail instead of prompting for input)",
191
+ )
192
+ parser.add_argument(
193
+ "--emsdk-headers",
194
+ type=str,
195
+ default=None,
196
+ help="Export EMSDK headers ZIP to specified path",
197
+ )
198
+
155
199
  build_mode = parser.add_mutually_exclusive_group()
156
200
  build_mode.add_argument("--debug", action="store_true", help="Build in debug mode")
157
201
  build_mode.add_argument(
@@ -167,6 +211,38 @@ def parse_args() -> Args:
167
211
 
168
212
  args = parser.parse_args()
169
213
 
214
+ # Handle --emsdk-headers early before other processing
215
+ if args.emsdk_headers:
216
+ from fastled.header_dump import dump_emsdk_headers
217
+
218
+ out_path = args.emsdk_headers
219
+ dump_emsdk_headers(out_path)
220
+ sys.exit(0)
221
+
222
+ # Auto-enable app mode if debug is used and Playwright cache exists
223
+ if args.debug and not args.app:
224
+ playwright_dir = Path.home() / ".fastled" / "playwright"
225
+ if playwright_dir.exists() and any(playwright_dir.iterdir()):
226
+ from fastled.emoji_util import EMO
227
+
228
+ print(
229
+ f"{EMO('⚠️', 'WARNING:')} Debug mode detected with Playwright installed - automatically enabling app mode"
230
+ )
231
+ args.app = True
232
+ elif not args.no_interactive:
233
+ # Prompt user to install Playwright only if not in no-interactive mode
234
+ answer = (
235
+ input("Would you like to install the FastLED debugger? [y/n] ")
236
+ .strip()
237
+ .lower()
238
+ )
239
+ if answer in ["y", "yes"]:
240
+ print(
241
+ "📦 To install Playwright, run: pip install playwright && python -m playwright install"
242
+ )
243
+ print("Then run your command again with --app flag")
244
+ sys.exit(0)
245
+
170
246
  # TODO: propagate the library.
171
247
  # from fastled.docker_manager import force_remove_previous
172
248
 
@@ -186,6 +262,11 @@ def parse_args() -> Args:
186
262
  # print(msg)
187
263
  # set_ramdisk_size(args.ram_disk_size)
188
264
 
265
+ # Handle --install early before other processing
266
+ if args.install:
267
+ # Don't process other arguments when --install is used
268
+ return Args.from_namespace(args)
269
+
189
270
  if args.purge:
190
271
  from fastled.docker_manager import DockerManager
191
272
 
@@ -268,6 +349,12 @@ def parse_args() -> Args:
268
349
  if cwd_is_fastled and not args.web and not args.server:
269
350
  print("Forcing --local mode because we are in the FastLED repo")
270
351
  args.localhost = True
352
+ if args.no_platformio:
353
+ print(
354
+ "--no-platformio mode enabled: forcing local Docker compilation to bypass PlatformIO constraints"
355
+ )
356
+ args.localhost = True
357
+ args.web = None # Clear web flag to ensure local compilation
271
358
  if args.localhost:
272
359
  args.web = "localhost"
273
360
  if args.interactive and not args.server:
@@ -282,7 +369,7 @@ def parse_args() -> Args:
282
369
  print("Searching for sketch directories...")
283
370
  sketch_directories = find_sketch_directories(maybe_sketch_dir)
284
371
  selected_dir = select_sketch_directory(
285
- sketch_directories, cwd_is_fastled
372
+ sketch_directories, cwd_is_fastled, is_followup=True
286
373
  )
287
374
  if selected_dir:
288
375
  print(f"Using sketch directory: {selected_dir}")
@@ -292,10 +379,24 @@ def parse_args() -> Args:
292
379
  "\nYou either need to specify a sketch directory or run in --server mode."
293
380
  )
294
381
  sys.exit(1)
295
- elif args.directory is not None and os.path.isfile(args.directory):
296
- dir_path = Path(args.directory).parent
297
- if looks_like_sketch_directory(dir_path):
298
- print(f"Using sketch directory: {dir_path}")
299
- args.directory = str(dir_path)
382
+ elif args.directory is not None:
383
+ # Check if directory is a file path
384
+ if os.path.isfile(args.directory):
385
+ dir_path = Path(args.directory).parent
386
+ if looks_like_sketch_directory(dir_path):
387
+ print(f"Using sketch directory: {dir_path}")
388
+ args.directory = str(dir_path)
389
+ # Check if directory exists as a path
390
+ elif not os.path.exists(args.directory):
391
+ # Directory doesn't exist - try partial name matching
392
+ try:
393
+ matched_dir = find_sketch_by_partial_name(args.directory)
394
+ print(
395
+ f"Matched '{args.directory}' to sketch directory: {matched_dir}"
396
+ )
397
+ args.directory = str(matched_dir)
398
+ except ValueError as e:
399
+ print(f"Error: {e}")
400
+ sys.exit(1)
300
401
 
301
402
  return Args.from_namespace(args)
@@ -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")