fastled 1.2.33__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 (58) hide show
  1. fastled/__init__.py +51 -192
  2. fastled/__main__.py +14 -0
  3. fastled/__version__.py +6 -0
  4. fastled/app.py +124 -27
  5. fastled/args.py +124 -0
  6. fastled/assets/localhost-key.pem +28 -0
  7. fastled/assets/localhost.pem +27 -0
  8. fastled/cli.py +10 -2
  9. fastled/cli_test.py +21 -0
  10. fastled/cli_test_interactive.py +21 -0
  11. fastled/client_server.py +334 -55
  12. fastled/compile_server.py +12 -1
  13. fastled/compile_server_impl.py +115 -42
  14. fastled/docker_manager.py +392 -69
  15. fastled/emoji_util.py +27 -0
  16. fastled/filewatcher.py +100 -8
  17. fastled/find_good_connection.py +105 -0
  18. fastled/header_dump.py +63 -0
  19. fastled/install/__init__.py +1 -0
  20. fastled/install/examples_manager.py +62 -0
  21. fastled/install/extension_manager.py +113 -0
  22. fastled/install/main.py +156 -0
  23. fastled/install/project_detection.py +167 -0
  24. fastled/install/test_install.py +373 -0
  25. fastled/install/vscode_config.py +344 -0
  26. fastled/interruptible_http.py +148 -0
  27. fastled/keyboard.py +1 -0
  28. fastled/keyz.py +84 -0
  29. fastled/live_client.py +26 -1
  30. fastled/open_browser.py +133 -89
  31. fastled/parse_args.py +219 -15
  32. fastled/playwright/chrome_extension_downloader.py +207 -0
  33. fastled/playwright/playwright_browser.py +773 -0
  34. fastled/playwright/resize_tracking.py +127 -0
  35. fastled/print_filter.py +52 -0
  36. fastled/project_init.py +20 -13
  37. fastled/select_sketch_directory.py +142 -17
  38. fastled/server_flask.py +487 -0
  39. fastled/server_start.py +21 -0
  40. fastled/settings.py +53 -4
  41. fastled/site/build.py +2 -10
  42. fastled/site/examples.py +10 -0
  43. fastled/sketch.py +129 -7
  44. fastled/string_diff.py +218 -9
  45. fastled/test/examples.py +7 -5
  46. fastled/types.py +22 -2
  47. fastled/util.py +78 -0
  48. fastled/version.py +41 -0
  49. fastled/web_compile.py +401 -218
  50. fastled/zip_files.py +76 -0
  51. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/METADATA +533 -382
  52. fastled-1.4.50.dist-info/RECORD +60 -0
  53. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/WHEEL +1 -1
  54. fastled/open_browser2.py +0 -111
  55. fastled-1.2.33.dist-info/RECORD +0 -33
  56. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/entry_points.txt +0 -0
  57. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info/licenses}/LICENSE +0 -0
  58. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/top_level.txt +0 -0
fastled/emoji_util.py ADDED
@@ -0,0 +1,27 @@
1
+ """
2
+ Emoji utility functions for handling Unicode display issues on Windows cmd.exe
3
+ """
4
+
5
+ import sys
6
+
7
+
8
+ def EMO(emoji: str, fallback: str) -> str:
9
+ """Get emoji with fallback for systems that don't support Unicode properly"""
10
+ try:
11
+ # Test if we can encode the emoji properly
12
+ emoji.encode(sys.stdout.encoding or "utf-8")
13
+ return emoji
14
+ except (UnicodeEncodeError, AttributeError):
15
+ return fallback
16
+
17
+
18
+ def safe_print(text: str) -> None:
19
+ """Print text safely, handling Unicode/emoji encoding issues on Windows cmd.exe"""
20
+ try:
21
+ print(text)
22
+ except UnicodeEncodeError:
23
+ # Replace problematic characters with safe alternatives
24
+ safe_text = text.encode(
25
+ sys.stdout.encoding or "utf-8", errors="replace"
26
+ ).decode(sys.stdout.encoding or "utf-8")
27
+ print(safe_text)
fastled/filewatcher.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """File system watcher implementation using watchdog"""
2
2
 
3
+ import _thread
3
4
  import hashlib
4
5
  import os
5
6
  import queue
@@ -15,10 +16,14 @@ from watchdog.events import FileSystemEvent, FileSystemEventHandler
15
16
  from watchdog.observers import Observer
16
17
  from watchdog.observers.api import BaseObserver
17
18
 
19
+ from fastled.settings import FILE_CHANGED_DEBOUNCE_SECONDS
20
+
21
+ _WATCHER_TIMEOUT = 0.1
22
+
18
23
 
19
24
  def file_watcher_enabled() -> bool:
20
- """Check if watchdog is disabled"""
21
- return os.getenv("NO_FILE_WATCHING", "0") == "1"
25
+ """Check if watchdog is enabled"""
26
+ return os.getenv("NO_FILE_WATCHING", "0") != "1"
22
27
 
23
28
 
24
29
  def file_watcher_set(enabled: bool) -> None:
@@ -68,7 +73,7 @@ class FileChangedNotifier(threading.Thread):
68
73
  def __init__(
69
74
  self,
70
75
  path: str,
71
- debounce_seconds: float = 1.0,
76
+ debounce_seconds: float = FILE_CHANGED_DEBOUNCE_SECONDS,
72
77
  excluded_patterns: list[str] | None = None,
73
78
  ) -> None:
74
79
  """Initialize the notifier with a path to watch
@@ -117,10 +122,13 @@ class FileChangedNotifier(threading.Thread):
117
122
  time.sleep(0.1)
118
123
  except KeyboardInterrupt:
119
124
  print("File watcher stopped by user.")
125
+ import _thread
126
+
127
+ _thread.interrupt_main()
120
128
  finally:
121
129
  self.stop()
122
130
 
123
- def get_next_change(self, timeout: float = 0.001) -> str | None:
131
+ def get_next_change(self, timeout: float = _WATCHER_TIMEOUT) -> str | None:
124
132
  """Get the next file change event from the queue
125
133
 
126
134
  Args:
@@ -129,7 +137,7 @@ class FileChangedNotifier(threading.Thread):
129
137
  Returns:
130
138
  Changed filepath or None if no change within timeout
131
139
  """
132
- if file_watcher_enabled():
140
+ if not file_watcher_enabled():
133
141
  time.sleep(timeout)
134
142
  return None
135
143
  try:
@@ -148,7 +156,7 @@ class FileChangedNotifier(threading.Thread):
148
156
  except queue.Empty:
149
157
  return None
150
158
 
151
- def get_all_changes(self, timeout: float = 0.001) -> list[str]:
159
+ def get_all_changes(self, timeout: float = _WATCHER_TIMEOUT) -> list[str]:
152
160
  """Get all file change events from the queue
153
161
 
154
162
  Args:
@@ -185,15 +193,26 @@ def _process_wrapper(root: Path, excluded_patterns: list[str], queue: Queue):
185
193
  watcher.stop()
186
194
 
187
195
 
196
+ class ProcessWraperTask:
197
+ def __init__(self, root: Path, excluded_patterns: list[str], queue: Queue) -> None:
198
+ self.root = root
199
+ self.excluded_patterns = excluded_patterns
200
+ self.queue = queue
201
+
202
+ def run(self):
203
+ _process_wrapper(self.root, self.excluded_patterns, self.queue)
204
+
205
+
188
206
  class FileWatcherProcess:
189
207
  def __init__(self, root: Path, excluded_patterns: list[str]) -> None:
190
208
  self.queue: Queue = Queue()
209
+ task = ProcessWraperTask(root, excluded_patterns, self.queue)
191
210
  self.process = Process(
192
- target=_process_wrapper,
193
- args=(root, excluded_patterns, self.queue),
211
+ target=task.run,
194
212
  daemon=True,
195
213
  )
196
214
  self.process.start()
215
+ self.global_debounce = FILE_CHANGED_DEBOUNCE_SECONDS
197
216
 
198
217
  def stop(self):
199
218
  self.process.terminate()
@@ -212,3 +231,76 @@ class FileWatcherProcess:
212
231
  except Empty:
213
232
  break
214
233
  return changed_files
234
+
235
+
236
+ class DebouncedFileWatcherProcess(threading.Thread):
237
+ """
238
+ Wraps a FileWatcherProcess to batch rapid-fire change events
239
+ and only emit them once the debounce interval has passed.
240
+ Runs in its own thread, polling every 0.1 s.
241
+ """
242
+
243
+ def __init__(
244
+ self,
245
+ watcher: FileWatcherProcess,
246
+ debounce_seconds: float = FILE_CHANGED_DEBOUNCE_SECONDS,
247
+ ) -> None:
248
+ super().__init__(daemon=True)
249
+ self.watcher = watcher
250
+ self.debounce_seconds = debounce_seconds
251
+
252
+ # internal buffer & timing
253
+ self._buffer: list[str] = []
254
+ self._last_time: float = 0.0
255
+
256
+ # queue of flushed batches
257
+ self._out_queue: queue.Queue[list[str]] = queue.Queue()
258
+ self._stop_event = threading.Event()
259
+
260
+ # record timestamp of last event for external inspection
261
+ self.last_event_time: float | None = None
262
+
263
+ self.start()
264
+
265
+ def run(self) -> None:
266
+ try:
267
+ while not self._stop_event.is_set():
268
+ now = time.time()
269
+ # non-blocking poll of raw watcher events
270
+ new = self.watcher.get_all_changes(timeout=0)
271
+ if new:
272
+ self._buffer.extend(new)
273
+ self._last_time = now
274
+ self.last_event_time = now
275
+
276
+ # if buffer exists and debounce interval elapsed, flush it
277
+ if self._buffer and (now - self._last_time) > self.debounce_seconds:
278
+ batch = sorted(set(self._buffer))
279
+ self._buffer.clear()
280
+ self._last_time = now
281
+ self._out_queue.put(batch)
282
+
283
+ time.sleep(0.1)
284
+ except KeyboardInterrupt:
285
+ _thread.interrupt_main()
286
+ raise
287
+
288
+ def get_all_changes(self, timeout: float | None = None) -> list[str]:
289
+ """
290
+ Return the next debounced batch of paths, or [] if none arrives
291
+ within `timeout` seconds.
292
+ """
293
+ try:
294
+ if timeout is None:
295
+ # non-blocking
296
+ return self._out_queue.get_nowait()
297
+ else:
298
+ return self._out_queue.get(timeout=timeout)
299
+ except queue.Empty:
300
+ return []
301
+
302
+ def stop(self) -> None:
303
+ """Stop the polling thread and tear down the underlying watcher."""
304
+ self._stop_event.set()
305
+ self.join()
306
+ self.watcher.stop()
@@ -0,0 +1,105 @@
1
+ import _thread
2
+ import time
3
+ from concurrent.futures import Future, ThreadPoolExecutor, as_completed
4
+ from dataclasses import dataclass
5
+ from typing import Dict, Tuple
6
+
7
+ import httpx
8
+
9
+ _TIMEOUT = 30.0
10
+
11
+ _EXECUTOR = ThreadPoolExecutor(max_workers=8)
12
+
13
+ # In-memory cache for connection results
14
+ # Key: (tuple of urls, filter_out_bad, use_ipv6)
15
+ # Value: (ConnectionResult | None, timestamp)
16
+ _CONNECTION_CACHE: Dict[
17
+ Tuple[tuple, bool, bool], Tuple["ConnectionResult | None", float]
18
+ ] = {}
19
+ _CACHE_TTL = 60.0 * 60.0 # Cache results for 1 hour
20
+
21
+
22
+ @dataclass
23
+ class ConnectionResult:
24
+ host: str
25
+ success: bool
26
+ ipv4: bool
27
+
28
+
29
+ def _sanitize_host(host: str) -> str:
30
+ if host.startswith("http"):
31
+ return host
32
+ is_local_host = "localhost" in host or "127.0.0.1" in host or "0.0.0.0" in host
33
+ use_https = not is_local_host
34
+ if use_https:
35
+ return host if host.startswith("https://") else f"https://{host}"
36
+ return host if host.startswith("http://") else f"http://{host}"
37
+
38
+
39
+ def _test_connection(host: str, use_ipv4: bool) -> ConnectionResult:
40
+ # Function static cache
41
+ host = _sanitize_host(host)
42
+ transport = httpx.HTTPTransport(local_address="0.0.0.0") if use_ipv4 else None
43
+ result: ConnectionResult | None = None
44
+ try:
45
+ with httpx.Client(
46
+ timeout=_TIMEOUT,
47
+ transport=transport,
48
+ ) as test_client:
49
+ test_response = test_client.get(
50
+ f"{host}/healthz", timeout=3, follow_redirects=True
51
+ )
52
+ result = ConnectionResult(host, test_response.status_code == 200, use_ipv4)
53
+ except KeyboardInterrupt:
54
+ _thread.interrupt_main()
55
+ result = ConnectionResult(host, False, use_ipv4)
56
+ except TimeoutError:
57
+ result = ConnectionResult(host, False, use_ipv4)
58
+ except Exception:
59
+ result = ConnectionResult(host, False, use_ipv4)
60
+ return result
61
+
62
+
63
+ def find_good_connection(
64
+ urls: list[str], filter_out_bad=True, use_ipv6: bool = True
65
+ ) -> ConnectionResult | None:
66
+ # Create cache key from parameters
67
+ cache_key = (tuple(sorted(urls)), filter_out_bad, use_ipv6)
68
+ current_time = time.time()
69
+
70
+ # Check if we have a cached result
71
+ if cache_key in _CONNECTION_CACHE:
72
+ cached_result, cached_time = _CONNECTION_CACHE[cache_key]
73
+ if current_time - cached_time < _CACHE_TTL:
74
+ return cached_result
75
+ else:
76
+ # Remove expired cache entry
77
+ del _CONNECTION_CACHE[cache_key]
78
+
79
+ # No valid cache entry, perform the actual connection test
80
+ futures: list[Future] = []
81
+ for url in urls:
82
+
83
+ f = _EXECUTOR.submit(_test_connection, url, use_ipv4=True)
84
+ futures.append(f)
85
+ if use_ipv6 and "localhost" not in url:
86
+ f_v6 = _EXECUTOR.submit(_test_connection, url, use_ipv4=False)
87
+ futures.append(f_v6)
88
+
89
+ result = None
90
+ try:
91
+ # Return first successful result
92
+ for future in as_completed(futures):
93
+ connection_result: ConnectionResult = future.result()
94
+ if connection_result.success or not filter_out_bad:
95
+ result = connection_result
96
+ break
97
+ finally:
98
+ # Cancel any remaining futures
99
+ for future in futures:
100
+ future.cancel()
101
+
102
+ # Cache the result (even if None)
103
+ _CONNECTION_CACHE[cache_key] = (result, current_time)
104
+
105
+ return result
fastled/header_dump.py ADDED
@@ -0,0 +1,63 @@
1
+ """Header dump functionality for EMSDK headers export."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+
7
+ def dump_emsdk_headers(output_path: Path | str, server_url: str | None = None) -> None:
8
+ """
9
+ Dump EMSDK headers to a specified path.
10
+
11
+ Args:
12
+ output_path: Path where to save the headers ZIP file
13
+ server_url: URL of the server. If None, tries to create local server first,
14
+ then falls back to remote server if local fails.
15
+ """
16
+ from fastled import Api
17
+ from fastled.settings import DEFAULT_URL
18
+ from fastled.util import download_emsdk_headers
19
+
20
+ # Convert to Path if string
21
+ if isinstance(output_path, str):
22
+ output_path = Path(output_path)
23
+
24
+ ends_with_zip = output_path.suffix == ".zip"
25
+ if not ends_with_zip:
26
+ raise ValueError(f"{output_path} must end with .zip")
27
+
28
+ try:
29
+ if server_url is not None:
30
+ # Use the provided server URL
31
+ download_emsdk_headers(server_url, output_path)
32
+ print(f"SUCCESS: EMSDK headers exported to {output_path}")
33
+ else:
34
+ # Try to create local server first
35
+ try:
36
+ with Api.server() as server:
37
+ base_url = server.url()
38
+ download_emsdk_headers(base_url, output_path)
39
+ print(
40
+ f"SUCCESS: EMSDK headers exported to {output_path} (using local server)"
41
+ )
42
+ except Exception as local_error:
43
+ print(
44
+ f"WARNING: Local server failed ({local_error}), falling back to remote server"
45
+ )
46
+ # Fall back to remote server
47
+ download_emsdk_headers(DEFAULT_URL, output_path)
48
+ print(
49
+ f"SUCCESS: EMSDK headers exported to {output_path} (using remote server)"
50
+ )
51
+
52
+ except Exception as e:
53
+ print(f"ERROR: Exception during header dump: {e}")
54
+ sys.exit(1)
55
+
56
+
57
+ if __name__ == "__main__":
58
+ if len(sys.argv) != 2:
59
+ print("Usage: python -m fastled.header_dump <output_path>")
60
+ sys.exit(1)
61
+
62
+ output_path = sys.argv[1]
63
+ dump_emsdk_headers(output_path)
@@ -0,0 +1 @@
1
+ """FastLED install module."""
@@ -0,0 +1,62 @@
1
+ """Examples installation manager using FastLED's built-in --project-init command."""
2
+
3
+ import subprocess
4
+
5
+
6
+ def install_fastled_examples_via_project_init(
7
+ force: bool = False, no_interactive: bool = False
8
+ ) -> bool:
9
+ """
10
+ Install FastLED examples using built-in --project-init command.
11
+
12
+ Args:
13
+ force: If True, install without prompting
14
+ no_interactive: If True, skip prompting and return False
15
+
16
+ Returns:
17
+ True if installation successful, False otherwise
18
+ """
19
+ if not force:
20
+ if no_interactive:
21
+ print("⚠️ No existing Arduino content found.")
22
+ print(" In non-interactive mode, skipping examples installation.")
23
+ print(" Run 'fastled --project-init' manually to install examples.")
24
+ return False
25
+
26
+ print("No existing Arduino content found.")
27
+ answer = (
28
+ input("Would you like to install FastLED examples? [y/n] ").strip().lower()
29
+ )
30
+ if answer not in ["y", "yes"]:
31
+ print("Skipping FastLED examples installation.")
32
+ return False
33
+
34
+ print("📦 Installing FastLED examples using project initialization...")
35
+
36
+ try:
37
+ # Use FastLED's built-in project initialization
38
+ subprocess.run(
39
+ ["fastled", "--project-init"],
40
+ check=True,
41
+ capture_output=True,
42
+ text=True,
43
+ cwd=".",
44
+ )
45
+
46
+ print("✅ FastLED project initialized successfully!")
47
+ print("📁 Examples and project structure created")
48
+ print("🚀 Quick start: Check for generated .ino files and press F5 to debug")
49
+
50
+ return True
51
+
52
+ except subprocess.CalledProcessError as e:
53
+ print(f"⚠️ Warning: Failed to initialize FastLED project: {e}")
54
+ if e.stderr:
55
+ print(f"Error details: {e.stderr}")
56
+ print("You can manually run: fastled --project-init")
57
+ return False
58
+ except FileNotFoundError:
59
+ print("⚠️ Warning: FastLED package not found. Please install it first:")
60
+ print(" pip install fastled")
61
+ print("Then run: fastled --project-init")
62
+ return False
@@ -0,0 +1,113 @@
1
+ """Auto Debug extension installation manager."""
2
+
3
+ import shutil
4
+ import subprocess
5
+ import tempfile
6
+ from pathlib import Path
7
+ from urllib.request import urlretrieve
8
+
9
+
10
+ def download_auto_debug_extension() -> Path | None:
11
+ """
12
+ Download the Auto Debug extension .vsix file from GitHub.
13
+
14
+ Returns:
15
+ Path to downloaded .vsix file, or None if download fails
16
+ """
17
+ # URL for the Auto Debug extension
18
+ extension_url = "https://github.com/zackees/vscode-auto-debug/releases/latest/download/auto-debug.vsix"
19
+
20
+ try:
21
+ # Create temporary directory
22
+ temp_dir = Path(tempfile.mkdtemp())
23
+ vsix_path = temp_dir / "auto-debug.vsix"
24
+
25
+ print("📥 Downloading Auto Debug extension...")
26
+
27
+ # Download the file
28
+ urlretrieve(extension_url, vsix_path)
29
+
30
+ if vsix_path.exists() and vsix_path.stat().st_size > 0:
31
+ print("✅ Extension downloaded successfully")
32
+ return vsix_path
33
+ else:
34
+ print("❌ Failed to download extension")
35
+ return None
36
+
37
+ except Exception as e:
38
+ print(f"❌ Error downloading extension: {e}")
39
+ return None
40
+
41
+
42
+ def install_vscode_extensions(extension_path: Path) -> bool:
43
+ """
44
+ Install extension in VSCode or Cursor.
45
+
46
+ Args:
47
+ extension_path: Path to .vsix file
48
+
49
+ Returns:
50
+ True if installation successful, False otherwise
51
+ """
52
+ # Try VSCode first
53
+ if shutil.which("code"):
54
+ ide_command = "code"
55
+ ide_name = "VSCode"
56
+ elif shutil.which("cursor"):
57
+ ide_command = "cursor"
58
+ ide_name = "Cursor"
59
+ else:
60
+ print("❌ No supported IDE found (VSCode or Cursor)")
61
+ return False
62
+
63
+ try:
64
+ print(f"📦 Installing extension in {ide_name}...")
65
+
66
+ # Install the extension
67
+ subprocess.run(
68
+ [ide_command, "--install-extension", str(extension_path)],
69
+ capture_output=True,
70
+ text=True,
71
+ check=True,
72
+ )
73
+
74
+ print(f"✅ Auto Debug extension installed in {ide_name}")
75
+ return True
76
+
77
+ except subprocess.CalledProcessError as e:
78
+ print(f"❌ Failed to install extension: {e}")
79
+ if e.stderr:
80
+ print(f"Error details: {e.stderr}")
81
+ return False
82
+ finally:
83
+ # Clean up temporary file
84
+ if extension_path.exists():
85
+ try:
86
+ extension_path.unlink()
87
+ extension_path.parent.rmdir()
88
+ except Exception:
89
+ pass
90
+
91
+
92
+ def install_auto_debug_extension(dry_run: bool = False) -> bool:
93
+ """
94
+ Main function to download and install Auto Debug extension.
95
+
96
+ Args:
97
+ dry_run: If True, simulate installation without actually installing
98
+
99
+ Returns:
100
+ True if installation successful, False otherwise
101
+ """
102
+ if dry_run:
103
+ print("[DRY-RUN]: Would download and install Auto Debug extension")
104
+ print("[DRY-RUN]: NO PLUGIN INSTALLED")
105
+ return True
106
+
107
+ # Download extension
108
+ vsix_path = download_auto_debug_extension()
109
+ if not vsix_path:
110
+ return False
111
+
112
+ # Install extension
113
+ return install_vscode_extensions(vsix_path)
@@ -0,0 +1,156 @@
1
+ """Main installation orchestrator for FastLED --install feature."""
2
+
3
+ import sys
4
+
5
+ from .examples_manager import install_fastled_examples_via_project_init
6
+ from .extension_manager import install_auto_debug_extension
7
+ from .project_detection import (
8
+ check_existing_arduino_content,
9
+ detect_fastled_project,
10
+ is_fastled_repository,
11
+ validate_vscode_project,
12
+ )
13
+ from .vscode_config import (
14
+ generate_fastled_tasks,
15
+ update_launch_json_for_arduino,
16
+ update_vscode_settings_for_fastled,
17
+ )
18
+
19
+
20
+ def fastled_install(dry_run: bool = False, no_interactive: bool = False) -> bool:
21
+ """
22
+ Main installation function with dry-run support.
23
+
24
+ Args:
25
+ dry_run: If True, simulate installation without making changes
26
+ no_interactive: If True, fail instead of prompting for input
27
+
28
+ Returns:
29
+ True if installation successful, False otherwise
30
+ """
31
+ try:
32
+ print("🚀 Starting FastLED installation...")
33
+
34
+ # 1. Validate VSCode project or offer alternatives
35
+ if not validate_vscode_project(no_interactive):
36
+ return False
37
+
38
+ # 2. Detect project type
39
+ is_fastled_project = detect_fastled_project()
40
+ is_repository = is_fastled_repository()
41
+
42
+ if is_fastled_project:
43
+ if is_repository:
44
+ print(
45
+ "✅ Detected FastLED repository - will configure full development environment"
46
+ )
47
+ else:
48
+ print(
49
+ "✅ Detected external FastLED project - will configure Arduino environment"
50
+ )
51
+ else:
52
+ print(
53
+ "✅ Detected standard project - will configure basic Arduino environment"
54
+ )
55
+
56
+ # 3. Auto Debug extension (with prompt)
57
+ if not dry_run and not no_interactive:
58
+ answer = (
59
+ input(
60
+ "\nWould you like to install the plugin for FastLED (auto-debug)? [y/n] "
61
+ )
62
+ .strip()
63
+ .lower()
64
+ )
65
+ elif no_interactive:
66
+ print(
67
+ "\n⚠️ Skipping Auto Debug extension installation in non-interactive mode"
68
+ )
69
+ answer = "no"
70
+ else:
71
+ answer = "yes"
72
+ print("\n[DRY-RUN]: Simulating Auto Debug extension installation...")
73
+
74
+ if answer in ["y", "yes"]:
75
+ if not install_auto_debug_extension(dry_run):
76
+ print(
77
+ "⚠️ Warning: Auto Debug extension installation failed, continuing..."
78
+ )
79
+
80
+ # 4. Configure VSCode files
81
+ print("\n📝 Configuring VSCode files...")
82
+ update_launch_json_for_arduino()
83
+ generate_fastled_tasks()
84
+
85
+ # 5. Examples installation (conditional)
86
+ if not check_existing_arduino_content():
87
+ if no_interactive:
88
+ print(
89
+ "⚠️ No Arduino content found. In non-interactive mode, skipping examples installation."
90
+ )
91
+ print(" Run 'fastled --project-init' manually to install examples.")
92
+ else:
93
+ install_fastled_examples_via_project_init(no_interactive=no_interactive)
94
+ else:
95
+ print(
96
+ "✅ Existing Arduino content detected, skipping examples installation"
97
+ )
98
+
99
+ # 6. Full development setup (repository only)
100
+ if is_fastled_project:
101
+ if is_repository:
102
+ print("\n🔧 Setting up FastLED development environment...")
103
+ update_vscode_settings_for_fastled()
104
+ else:
105
+ print(
106
+ "\n⚠️ Skipping clangd settings - not in FastLED repository (protects your environment)"
107
+ )
108
+
109
+ # 7. Post-installation auto-execution
110
+ if not dry_run:
111
+ auto_execute_fastled()
112
+ else:
113
+ print("\n[DRY-RUN]: Skipping auto-execution")
114
+
115
+ print("\n✅ FastLED installation completed successfully!")
116
+ return True
117
+
118
+ except Exception as e:
119
+ print(f"\n❌ Installation failed: {e}")
120
+ import traceback
121
+
122
+ traceback.print_exc()
123
+ return False
124
+
125
+
126
+ def auto_execute_fastled() -> None:
127
+ """Auto-launch fastled after successful installation."""
128
+ if check_existing_arduino_content():
129
+ print("\n🚀 Auto-launching FastLED...")
130
+
131
+ # Import the main function to avoid circular imports
132
+ from fastled.app import main
133
+
134
+ # Filter out --install and --dry-run from sys.argv
135
+ original_argv = sys.argv.copy()
136
+ filtered_argv = [
137
+ arg for arg in sys.argv if arg not in ["--install", "--dry-run"]
138
+ ]
139
+
140
+ # If no directory specified, add current directory
141
+ if len(filtered_argv) == 1: # Only the command name
142
+ filtered_argv.append(".")
143
+
144
+ # Replace sys.argv temporarily
145
+ sys.argv = filtered_argv
146
+
147
+ try:
148
+ # Call main directly
149
+ main()
150
+ finally:
151
+ # Restore original argv
152
+ sys.argv = original_argv
153
+ else:
154
+ print(
155
+ "\n💡 No Arduino content found. Create some .ino files and run 'fastled' to compile!"
156
+ )