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.
- fastled/__init__.py +51 -192
- fastled/__main__.py +14 -0
- fastled/__version__.py +6 -0
- fastled/app.py +124 -27
- fastled/args.py +124 -0
- fastled/assets/localhost-key.pem +28 -0
- fastled/assets/localhost.pem +27 -0
- fastled/cli.py +10 -2
- fastled/cli_test.py +21 -0
- fastled/cli_test_interactive.py +21 -0
- fastled/client_server.py +334 -55
- fastled/compile_server.py +12 -1
- fastled/compile_server_impl.py +115 -42
- fastled/docker_manager.py +392 -69
- fastled/emoji_util.py +27 -0
- fastled/filewatcher.py +100 -8
- fastled/find_good_connection.py +105 -0
- fastled/header_dump.py +63 -0
- fastled/install/__init__.py +1 -0
- fastled/install/examples_manager.py +62 -0
- fastled/install/extension_manager.py +113 -0
- fastled/install/main.py +156 -0
- fastled/install/project_detection.py +167 -0
- fastled/install/test_install.py +373 -0
- fastled/install/vscode_config.py +344 -0
- fastled/interruptible_http.py +148 -0
- fastled/keyboard.py +1 -0
- fastled/keyz.py +84 -0
- fastled/live_client.py +26 -1
- fastled/open_browser.py +133 -89
- fastled/parse_args.py +219 -15
- fastled/playwright/chrome_extension_downloader.py +207 -0
- fastled/playwright/playwright_browser.py +773 -0
- fastled/playwright/resize_tracking.py +127 -0
- fastled/print_filter.py +52 -0
- fastled/project_init.py +20 -13
- fastled/select_sketch_directory.py +142 -17
- fastled/server_flask.py +487 -0
- fastled/server_start.py +21 -0
- fastled/settings.py +53 -4
- fastled/site/build.py +2 -10
- fastled/site/examples.py +10 -0
- fastled/sketch.py +129 -7
- fastled/string_diff.py +218 -9
- fastled/test/examples.py +7 -5
- fastled/types.py +22 -2
- fastled/util.py +78 -0
- fastled/version.py +41 -0
- fastled/web_compile.py +401 -218
- fastled/zip_files.py +76 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/METADATA +533 -382
- fastled-1.4.50.dist-info/RECORD +60 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/WHEEL +1 -1
- fastled/open_browser2.py +0 -111
- fastled-1.2.33.dist-info/RECORD +0 -33
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/entry_points.txt +0 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info/licenses}/LICENSE +0 -0
- {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
|
|
21
|
-
return os.getenv("NO_FILE_WATCHING", "0")
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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=
|
|
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)
|
fastled/install/main.py
ADDED
|
@@ -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
|
+
)
|