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
@@ -0,0 +1,344 @@
1
+ """VSCode configuration generation for FastLED projects."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+
7
+ def update_launch_json_for_arduino() -> None:
8
+ """Update launch.json with Arduino debugging configuration."""
9
+ launch_json_path = Path.cwd() / ".vscode" / "launch.json"
10
+
11
+ # Default launch configuration
12
+ arduino_config = {
13
+ "name": "🎯 Auto Debug (Smart File Detection)",
14
+ "type": "auto-debug",
15
+ "request": "launch",
16
+ "map": {
17
+ "*.ino": "Arduino: Run .ino with FastLED",
18
+ "*.py": "Python: Current File (UV)",
19
+ },
20
+ }
21
+
22
+ if launch_json_path.exists():
23
+ # Merge with existing
24
+ try:
25
+ with open(launch_json_path, "r") as f:
26
+ data = json.load(f)
27
+ except json.JSONDecodeError:
28
+ data = {"version": "0.2.0", "configurations": []}
29
+ else:
30
+ data = {"version": "0.2.0", "configurations": []}
31
+
32
+ # Check if configuration already exists
33
+ configs = data.get("configurations", [])
34
+ exists = any(cfg.get("name") == arduino_config["name"] for cfg in configs)
35
+
36
+ if not exists:
37
+ configs.insert(0, arduino_config) # Add at the beginning
38
+ data["configurations"] = configs
39
+
40
+ # Write back
41
+ launch_json_path.parent.mkdir(exist_ok=True)
42
+ with open(launch_json_path, "w") as f:
43
+ json.dump(data, f, indent=4)
44
+
45
+ print(f"✅ Updated {launch_json_path}")
46
+
47
+
48
+ def generate_fastled_tasks() -> None:
49
+ """Generate/update tasks.json with FastLED build tasks."""
50
+ tasks_json_path = Path.cwd() / ".vscode" / "tasks.json"
51
+
52
+ # FastLED tasks
53
+ fastled_tasks = [
54
+ {
55
+ "type": "shell",
56
+ "label": "Run FastLED (Debug)",
57
+ "command": "fastled",
58
+ "args": ["${file}", "--debug", "--app"],
59
+ "options": {"cwd": "${workspaceFolder}"},
60
+ "group": {"kind": "build", "isDefault": True},
61
+ "presentation": {
62
+ "echo": True,
63
+ "reveal": "always",
64
+ "focus": True,
65
+ "panel": "new",
66
+ "showReuseMessage": False,
67
+ "clear": True,
68
+ },
69
+ "detail": "Run FastLED with debug mode and app visualization",
70
+ "problemMatcher": [],
71
+ },
72
+ {
73
+ "type": "shell",
74
+ "label": "Run FastLED (Quick)",
75
+ "command": "fastled",
76
+ "args": ["${file}", "--background-update"],
77
+ "options": {"cwd": "${workspaceFolder}"},
78
+ "group": "build",
79
+ "presentation": {
80
+ "echo": True,
81
+ "reveal": "always",
82
+ "focus": True,
83
+ "panel": "new",
84
+ "showReuseMessage": False,
85
+ "clear": True,
86
+ },
87
+ "detail": "Run FastLED with quick background update mode",
88
+ "problemMatcher": [],
89
+ },
90
+ ]
91
+
92
+ if tasks_json_path.exists():
93
+ # Merge with existing
94
+ try:
95
+ with open(tasks_json_path, "r") as f:
96
+ data = json.load(f)
97
+ except json.JSONDecodeError:
98
+ data = {"version": "2.0.0", "tasks": []}
99
+ else:
100
+ data = {"version": "2.0.0", "tasks": []}
101
+
102
+ # Get existing tasks
103
+ existing_tasks = data.get("tasks", [])
104
+ existing_labels = {task.get("label") for task in existing_tasks}
105
+
106
+ # Add new tasks if they don't exist
107
+ for task in fastled_tasks:
108
+ if task["label"] not in existing_labels:
109
+ existing_tasks.append(task)
110
+
111
+ data["tasks"] = existing_tasks
112
+
113
+ # Write back
114
+ tasks_json_path.parent.mkdir(exist_ok=True)
115
+ with open(tasks_json_path, "w") as f:
116
+ json.dump(data, f, indent=4)
117
+
118
+ print(f"✅ Updated {tasks_json_path}")
119
+
120
+
121
+ def update_vscode_settings_for_fastled() -> None:
122
+ """
123
+ 🚨 Repository-only: Apply clangd settings and IntelliSense overrides.
124
+ This should ONLY be called for the actual FastLED repository.
125
+ """
126
+ from .project_detection import is_fastled_repository
127
+
128
+ # Safety check - only apply in actual repository
129
+ if not is_fastled_repository():
130
+ return
131
+ settings_json_path = Path.cwd() / ".vscode" / "settings.json"
132
+
133
+ # FastLED repository-specific settings - updated to match the official FastLED repo
134
+ fastled_settings = {
135
+ # Terminal configuration
136
+ "terminal.integrated.defaultProfile.windows": "Git Bash",
137
+ "terminal.integrated.shellIntegration.enabled": False,
138
+ "terminal.integrated.profiles.windows": {
139
+ "Command Prompt": {"path": "C:\\Windows\\System32\\cmd.exe"},
140
+ "Git Bash": {
141
+ "path": "C:\\Program Files\\Git\\bin\\bash.exe",
142
+ "args": ["--cd=."],
143
+ },
144
+ },
145
+ # File settings
146
+ "files.eol": "\n", # Unix line endings
147
+ "files.autoDetectEol": False, # Prevent VS Code from auto-detecting and changing EOL
148
+ "files.insertFinalNewline": True, # Ensure files end with a newline
149
+ "files.trimFinalNewlines": True, # Remove extra newlines at the end
150
+ "editor.tabSize": 4,
151
+ "editor.insertSpaces": True,
152
+ "editor.detectIndentation": True,
153
+ "editor.formatOnSave": False, # Disabled to prevent conflicts
154
+ # Debugger defaults - ensure C++ debugger is used for C++ files
155
+ "debug.defaultDebuggerType": "cppdbg",
156
+ "debug.toolBarLocation": "docked",
157
+ "debug.console.fontSize": 14,
158
+ "debug.console.lineHeight": 19,
159
+ # Python configuration (using uv as per project rules)
160
+ "python.defaultInterpreterPath": "uv",
161
+ "python.debugger": "debugpy",
162
+ # File associations for debugger
163
+ "[cpp]": {
164
+ "editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd",
165
+ "debug.defaultDebuggerType": "cppdbg",
166
+ },
167
+ "[c]": {
168
+ "editor.defaultFormatter": "ms-vscode.cpptools",
169
+ "debug.defaultDebuggerType": "cppdbg",
170
+ },
171
+ "[ino]": {
172
+ "editor.defaultFormatter": "ms-vscode.cpptools",
173
+ "debug.defaultDebuggerType": "cppdbg",
174
+ },
175
+ # clangd configuration - enhanced
176
+ "clangd.arguments": [
177
+ "--compile-commands-dir=${workspaceFolder}",
178
+ "--clang-tidy",
179
+ "--header-insertion=never",
180
+ "--completion-style=detailed",
181
+ "--function-arg-placeholders=false",
182
+ "--background-index",
183
+ "--pch-storage=memory",
184
+ ],
185
+ "clangd.fallbackFlags": [
186
+ "-std=c++17",
187
+ "-I${workspaceFolder}/src",
188
+ "-I${workspaceFolder}/tests",
189
+ "-Wno-global-constructors",
190
+ ],
191
+ # Disable conflicting IntelliSense to let clangd handle C++ analysis
192
+ "C_Cpp.intelliSenseEngine": "disabled",
193
+ "C_Cpp.autocomplete": "disabled",
194
+ "C_Cpp.errorSquiggles": "disabled",
195
+ "C_Cpp.suggestSnippets": False,
196
+ "C_Cpp.intelliSenseEngineFallback": "disabled",
197
+ "C_Cpp.autocompleteAddParentheses": False,
198
+ "C_Cpp.formatting": "disabled",
199
+ "C_Cpp.vcpkg.enabled": False,
200
+ "C_Cpp.configurationWarnings": "disabled",
201
+ "C_Cpp.intelliSenseCachePath": "",
202
+ "C_Cpp.intelliSenseCacheSize": 0,
203
+ "C_Cpp.intelliSenseUpdateDelay": 0,
204
+ "C_Cpp.workspaceParsingPriority": "lowest",
205
+ "C_Cpp.disabled": True,
206
+ # File associations - comprehensive
207
+ "files.associations": {
208
+ "*.ino": "cpp",
209
+ "*.h": "cpp",
210
+ "*.hpp": "cpp",
211
+ "*.cpp": "cpp",
212
+ "*.c": "c",
213
+ "*.inc": "cpp",
214
+ "*.tcc": "cpp",
215
+ "*.embeddedhtml": "html",
216
+ # Core C++ standard library files
217
+ "compare": "cpp",
218
+ "type_traits": "cpp",
219
+ "cmath": "cpp",
220
+ "limits": "cpp",
221
+ "iostream": "cpp",
222
+ "random": "cpp",
223
+ "functional": "cpp",
224
+ "bit": "cpp",
225
+ "vector": "cpp",
226
+ "array": "cpp",
227
+ "string": "cpp",
228
+ "memory": "cpp",
229
+ "algorithm": "cpp",
230
+ "iterator": "cpp",
231
+ "utility": "cpp",
232
+ "optional": "cpp",
233
+ "variant": "cpp",
234
+ "numeric": "cpp",
235
+ "chrono": "cpp",
236
+ "thread": "cpp",
237
+ "mutex": "cpp",
238
+ "atomic": "cpp",
239
+ "future": "cpp",
240
+ "condition_variable": "cpp",
241
+ },
242
+ # Disable Java language support and popups
243
+ "java.enabled": False,
244
+ "java.jdt.ls.enabled": False,
245
+ "java.compile.nullAnalysis.mode": "disabled",
246
+ "java.configuration.checkProjectSettingsExclusions": False,
247
+ "java.import.gradle.enabled": False,
248
+ "java.import.maven.enabled": False,
249
+ "java.autobuild.enabled": False,
250
+ "java.maxConcurrentBuilds": 0,
251
+ "java.recommendations.enabled": False,
252
+ "java.help.showReleaseNotes": False,
253
+ "redhat.telemetry.enabled": False,
254
+ # Java exclusions
255
+ "java.project.sourcePaths": [],
256
+ "java.project.referencedLibraries": [],
257
+ "files.exclude": {
258
+ "**/.classpath": True,
259
+ "**/.project": True,
260
+ "**/.factorypath": True,
261
+ },
262
+ # Disable PlatformIO auto-detection for .ino files in FastLED project
263
+ "platformio.disableToolchainAutoInstaller": True,
264
+ "platformio-ide.autoRebuildAutocompleteIndex": False,
265
+ "platformio-ide.activateProjectOnTextEditorChange": False,
266
+ "platformio-ide.autoOpenPlatformIOIniFile": False,
267
+ "platformio-ide.autoPreloadEnvTasks": False,
268
+ "platformio-ide.autoCloseSerialMonitor": False,
269
+ "platformio-ide.disablePIOHomeStartup": True,
270
+ # Disable conflicting extensions
271
+ "extensions.ignoreRecommendations": True,
272
+ # Semantic token color customizations for better C++ development
273
+ "editor.semanticTokenColorCustomizations": {
274
+ "rules": {
275
+ # Types (classes, structs, enums) - Teal/Cyan
276
+ "class": "#4EC9B0",
277
+ "struct": "#4EC9B0",
278
+ "type": "#4EC9B0",
279
+ "enum": "#4EC9B0",
280
+ "enumMember": "#B5CEA8",
281
+ "typedef": "#4EC9B0",
282
+ # Variables - Almost pure white for maximum readability
283
+ "variable": "#FAFAFA",
284
+ "variable.local": "#FAFAFA",
285
+ # Parameters - Orange for clear distinction
286
+ "parameter": "#FF8C42",
287
+ "variable.parameter": "#FF8C42",
288
+ # Properties - Light purple/pink
289
+ "property": "#D197D9",
290
+ # Functions and methods - Yellow
291
+ "function": "#DCDCAA",
292
+ "method": "#DCDCAA",
293
+ "function.declaration": "#DCDCAA",
294
+ "method.declaration": "#DCDCAA",
295
+ # Namespaces - Soft blue
296
+ "namespace": "#86C5F7",
297
+ # Constants and readonly - Light green with italic
298
+ "variable.readonly": {"foreground": "#B5CEA8", "fontStyle": "italic"},
299
+ "variable.defaultLibrary": "#B5CEA8",
300
+ # Macros and defines - Muted red/salmon
301
+ "macro": "#E06C75",
302
+ # String literals - Peach/salmon
303
+ "string": "#CE9178",
304
+ # Numbers - Light green
305
+ "number": "#B5CEA8",
306
+ # Keywords - Pink/magenta
307
+ "keyword": "#C586C0",
308
+ # Storage specifiers - Bright magenta/pink
309
+ "keyword.storage": "#FF79C6",
310
+ "storageClass": "#FF79C6",
311
+ # Built-in types - Different from user-defined types
312
+ "type.builtin": "#569CD6",
313
+ "keyword.type": "#569CD6",
314
+ # Comments - Green
315
+ "comment": "#6A9955",
316
+ "comment.documentation": "#6A9955",
317
+ }
318
+ },
319
+ # Inlay hints - Brighter gray for better visibility
320
+ "editor.inlayHints.fontColor": "#808080",
321
+ "editor.inlayHints.background": "#3C3C3C20",
322
+ }
323
+
324
+ if settings_json_path.exists():
325
+ # Merge with existing
326
+ try:
327
+ with open(settings_json_path, "r") as f:
328
+ data = json.load(f)
329
+ except json.JSONDecodeError:
330
+ data = {}
331
+ else:
332
+ data = {}
333
+
334
+ # Update settings
335
+ data.update(fastled_settings)
336
+
337
+ # Write back
338
+ settings_json_path.parent.mkdir(exist_ok=True)
339
+ with open(settings_json_path, "w") as f:
340
+ json.dump(data, f, indent=4)
341
+
342
+ print(
343
+ f"✅ Updated {settings_json_path} with comprehensive FastLED development settings"
344
+ )
@@ -0,0 +1,148 @@
1
+ """
2
+ Interruptible HTTP requests that can be cancelled with Ctrl+C.
3
+
4
+ This module provides cross-platform HTTP request functionality that can be
5
+ interrupted with Ctrl+C by using asyncio cancellation and periodic checks.
6
+ """
7
+
8
+ import asyncio
9
+
10
+ import httpx
11
+
12
+
13
+ class InterruptibleHTTPRequest:
14
+ """A wrapper for making HTTP requests that can be interrupted by Ctrl+C."""
15
+
16
+ def __init__(self):
17
+ self.cancelled = False
18
+
19
+ async def _make_request_async(
20
+ self,
21
+ url: str,
22
+ files: dict,
23
+ headers: dict,
24
+ transport: httpx.HTTPTransport | None = None,
25
+ timeout: float = 240,
26
+ follow_redirects: bool = True,
27
+ ) -> httpx.Response:
28
+ """Make an async HTTP request."""
29
+ # Convert sync transport to async transport if provided
30
+ async_transport = None
31
+ if transport is not None:
32
+ # For IPv4 connections, create async transport with local address
33
+ async_transport = httpx.AsyncHTTPTransport(local_address="0.0.0.0")
34
+
35
+ async with httpx.AsyncClient(
36
+ transport=async_transport,
37
+ timeout=timeout,
38
+ ) as client:
39
+ response = await client.post(
40
+ url,
41
+ follow_redirects=follow_redirects,
42
+ files=files,
43
+ headers=headers,
44
+ )
45
+ return response
46
+
47
+ def make_request_interruptible(
48
+ self,
49
+ url: str,
50
+ files: dict,
51
+ headers: dict,
52
+ transport: httpx.HTTPTransport | None = None,
53
+ timeout: float = 240,
54
+ follow_redirects: bool = True,
55
+ ) -> httpx.Response:
56
+ """Make an HTTP request that can be interrupted by Ctrl+C."""
57
+ try:
58
+ # Create a new event loop if we're not in one
59
+ try:
60
+ loop = asyncio.get_running_loop()
61
+ # We're already in an event loop, use run_in_executor
62
+ return asyncio.run_coroutine_threadsafe(
63
+ self._run_with_keyboard_check(
64
+ url, files, headers, transport, timeout, follow_redirects
65
+ ),
66
+ loop,
67
+ ).result()
68
+ except RuntimeError:
69
+ # No running loop, create one
70
+ return asyncio.run(
71
+ self._run_with_keyboard_check(
72
+ url, files, headers, transport, timeout, follow_redirects
73
+ )
74
+ )
75
+ except KeyboardInterrupt:
76
+ print("\nHTTP request cancelled by user")
77
+ raise
78
+
79
+ async def _run_with_keyboard_check(
80
+ self,
81
+ url: str,
82
+ files: dict,
83
+ headers: dict,
84
+ transport: httpx.HTTPTransport | None = None,
85
+ timeout: float = 240,
86
+ follow_redirects: bool = True,
87
+ ) -> httpx.Response:
88
+ """Run the request with periodic keyboard interrupt checks."""
89
+ task = asyncio.create_task(
90
+ self._make_request_async(
91
+ url, files, headers, transport, timeout, follow_redirects
92
+ )
93
+ )
94
+
95
+ # Poll for keyboard interrupt while waiting for the request
96
+ # This approach allows the task to be cancelled when KeyboardInterrupt
97
+ # is raised in the calling thread
98
+ while not task.done():
99
+ try:
100
+ # Wait for either completion or a short timeout
101
+ response = await asyncio.wait_for(asyncio.shield(task), timeout=0.1)
102
+ return response
103
+ except asyncio.TimeoutError:
104
+ # Continue waiting - the short timeout allows for more responsive
105
+ # cancellation when KeyboardInterrupt is raised
106
+ continue
107
+ except KeyboardInterrupt:
108
+ task.cancel()
109
+ print("\nHTTP request cancelled by user")
110
+ raise
111
+
112
+ return await task
113
+
114
+
115
+ def make_interruptible_post_request(
116
+ url: str,
117
+ files: dict | None = None,
118
+ headers: dict | None = None,
119
+ transport: httpx.HTTPTransport | None = None,
120
+ timeout: float = 240,
121
+ follow_redirects: bool = True,
122
+ ) -> httpx.Response:
123
+ """
124
+ Convenience function to make an interruptible POST request.
125
+
126
+ Args:
127
+ url: The URL to make the request to
128
+ files: Files to upload (optional)
129
+ headers: HTTP headers (optional)
130
+ transport: HTTP transport to use (optional)
131
+ timeout: Request timeout in seconds
132
+ follow_redirects: Whether to follow redirects
133
+
134
+ Returns:
135
+ The HTTP response
136
+
137
+ Raises:
138
+ KeyboardInterrupt: If the request was cancelled by Ctrl+C
139
+ """
140
+ request_handler = InterruptibleHTTPRequest()
141
+ return request_handler.make_request_interruptible(
142
+ url=url,
143
+ files=files or {},
144
+ headers=headers or {},
145
+ transport=transport,
146
+ timeout=timeout,
147
+ follow_redirects=follow_redirects,
148
+ )
fastled/live_client.py CHANGED
@@ -22,6 +22,8 @@ class LiveClient:
22
22
  keep_running: bool = True,
23
23
  build_mode: BuildMode = BuildMode.QUICK,
24
24
  profile: bool = False,
25
+ no_platformio: bool = False,
26
+ enable_https: bool = True, # Enable HTTPS for the local server
25
27
  ) -> None:
26
28
  self.sketch_directory = sketch_directory
27
29
  self.host = host
@@ -34,6 +36,8 @@ class LiveClient:
34
36
  self.shutdown = threading.Event()
35
37
  self.thread: threading.Thread | None = None
36
38
  self.auto_updates = auto_updates
39
+ self.no_platformio = no_platformio
40
+ self.enable_https = enable_https
37
41
  if auto_start:
38
42
  self.start()
39
43
  if self.auto_updates is False:
@@ -52,18 +56,22 @@ class LiveClient:
52
56
  profile=self.profile,
53
57
  shutdown=self.shutdown,
54
58
  http_port=self.http_port,
59
+ no_platformio=self.no_platformio,
60
+ enable_https=self.enable_https,
55
61
  )
56
62
  return rtn
57
63
 
58
64
  def url(self) -> str:
59
65
  """Get the URL of the server."""
66
+ from fastled.settings import SERVER_PORT
67
+
60
68
  if isinstance(self.host, CompileServer):
61
69
  return self.host.url()
62
70
  if self.host is None:
63
71
  import warnings
64
72
 
65
73
  warnings.warn("TODO: use the actual host.")
66
- return "http://localhost:9021"
74
+ return f"http://localhost:{SERVER_PORT}"
67
75
  return self.host
68
76
 
69
77
  @property
@@ -94,3 +102,15 @@ class LiveClient:
94
102
 
95
103
  def __exit__(self, exc_type, exc_value, traceback) -> None:
96
104
  self.finalize()
105
+
106
+ def get_emsdk_headers(self, filepath: Path) -> None:
107
+ """Get EMSDK headers ZIP data from the server and save to filepath."""
108
+ if isinstance(self.host, CompileServer):
109
+ self.host.get_emsdk_headers(filepath)
110
+ else:
111
+ # Handle string host or None case by using web_compile approach
112
+ from fastled.settings import DEFAULT_URL
113
+ from fastled.util import download_emsdk_headers
114
+
115
+ base_url = self.host if isinstance(self.host, str) else DEFAULT_URL
116
+ download_emsdk_headers(base_url, filepath)