fastled 1.1.20__py2.py3-none-any.whl → 1.1.22__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fastled/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """FastLED Wasm Compiler package."""
2
2
 
3
- __version__ = "1.1.20"
3
+ __version__ = "1.1.22"
fastled/app.py CHANGED
@@ -1,481 +1,187 @@
1
- """
2
- Uses the latest wasm compiler image to compile the FastLED sketch.
3
- """
4
-
5
- import argparse
6
- import os
7
- import platform
8
- import shutil
9
- import subprocess
10
- import sys
11
- import tempfile
12
- import time
13
- from dataclasses import dataclass
14
- from pathlib import Path
15
-
16
- from fastled import __version__
17
- from fastled.build_mode import BuildMode, get_build_mode
18
- from fastled.compile_server import CompileServer
19
- from fastled.docker_manager import DockerManager
20
- from fastled.filewatcher import FileWatcherProcess
21
- from fastled.keyboard import SpaceBarWatcher
22
- from fastled.open_browser import open_browser_thread
23
- from fastled.sketch import (
24
- find_sketch_directories,
25
- looks_like_fastled_repo,
26
- looks_like_sketch_directory,
27
- )
28
- from fastled.web_compile import (
29
- SERVER_PORT,
30
- ConnectionResult,
31
- find_good_connection,
32
- web_compile,
33
- )
34
-
35
- machine = platform.machine().lower()
36
- IS_ARM: bool = "arm" in machine or "aarch64" in machine
37
- PLATFORM_TAG: str = "-arm64" if IS_ARM else ""
38
- CONTAINER_NAME = f"fastled-wasm-compiler{PLATFORM_TAG}"
39
- DEFAULT_URL = "https://fastled.onrender.com"
40
-
41
-
42
- @dataclass
43
- class CompiledResult:
44
- """Dataclass to hold the result of the compilation."""
45
-
46
- success: bool
47
- fastled_js: str
48
- hash_value: str | None
49
-
50
-
51
- def parse_args() -> argparse.Namespace:
52
- """Parse command-line arguments."""
53
- parser = argparse.ArgumentParser(description=f"FastLED WASM Compiler {__version__}")
54
- parser.add_argument(
55
- "--version", action="version", version=f"%(prog)s {__version__}"
56
- )
57
- parser.add_argument(
58
- "directory",
59
- type=str,
60
- nargs="?",
61
- default=None,
62
- help="Directory containing the FastLED sketch to compile",
63
- )
64
- parser.add_argument(
65
- "--just-compile",
66
- action="store_true",
67
- help="Just compile, skip opening the browser and watching for changes.",
68
- )
69
- parser.add_argument(
70
- "--web",
71
- "-w",
72
- type=str,
73
- nargs="?",
74
- # const does not seem to be working as expected
75
- const=DEFAULT_URL, # Default value when --web is specified without value
76
- help="Use web compiler. Optional URL can be provided (default: https://fastled.onrender.com)",
77
- )
78
- parser.add_argument(
79
- "-i",
80
- "--interactive",
81
- action="store_true",
82
- help="Run in interactive mode (Not available with --web)",
83
- )
84
- parser.add_argument(
85
- "--profile",
86
- action="store_true",
87
- help="Enable profiling for web compilation",
88
- )
89
- parser.add_argument(
90
- "--force-compile",
91
- action="store_true",
92
- help="Skips the test to see if the current directory is a valid FastLED sketch directory",
93
- )
94
- parser.add_argument(
95
- "--no-auto-updates",
96
- action="store_true",
97
- help="Disable automatic updates of the wasm compiler image when using docker.",
98
- )
99
- parser.add_argument(
100
- "--update",
101
- action="store_true",
102
- help="Update the wasm compiler (if necessary) before running",
103
- )
104
- parser.add_argument(
105
- "--localhost",
106
- "--local",
107
- "-l",
108
- action="store_true",
109
- help="Use localhost for web compilation from an instance of fastled --server, creating it if necessary",
110
- )
111
- parser.add_argument(
112
- "--server",
113
- "-s",
114
- action="store_true",
115
- help="Run the server in the current directory, volume mapping fastled if we are in the repo",
116
- )
117
- build_mode = parser.add_mutually_exclusive_group()
118
- build_mode.add_argument("--debug", action="store_true", help="Build in debug mode")
119
- build_mode.add_argument(
120
- "--quick",
121
- action="store_true",
122
- default=True,
123
- help="Build in quick mode (default)",
124
- )
125
- build_mode.add_argument(
126
- "--release", action="store_true", help="Build in release mode"
127
- )
128
-
129
- cwd_is_fastled = looks_like_fastled_repo(Path(os.getcwd()))
130
-
131
- args = parser.parse_args()
132
- if args.update:
133
- args.auto_update = True
134
- elif args.no_auto_updates:
135
- args.auto_update = False
136
- else:
137
- args.auto_update = None
138
-
139
- if not cwd_is_fastled and not args.localhost and not args.web and not args.server:
140
- print(f"Using web compiler at {DEFAULT_URL}")
141
- args.web = DEFAULT_URL
142
- if cwd_is_fastled and not args.web and not args.server:
143
- print("Forcing --local mode because we are in the FastLED repo")
144
- args.localhost = True
145
- if args.localhost:
146
- args.web = "localhost"
147
- if args.interactive and not args.server:
148
- print("--interactive forces --server mode")
149
- args.server = True
150
- if args.directory is None and not args.server:
151
- # does current directory look like a sketch?
152
- maybe_sketch_dir = Path(os.getcwd())
153
- if looks_like_sketch_directory(maybe_sketch_dir):
154
- args.directory = str(maybe_sketch_dir)
155
- else:
156
- sketch_directories = find_sketch_directories(maybe_sketch_dir)
157
- if len(sketch_directories) == 1:
158
- print(f"\nUsing sketch directory: {sketch_directories[0]}")
159
- args.directory = str(sketch_directories[0])
160
- elif len(sketch_directories) > 1:
161
- print("\nMultiple Directories found, choose one:")
162
- for i, sketch_dir in enumerate(sketch_directories):
163
- print(f" [{i+1}]: {sketch_dir}")
164
- which = input("\nPlease specify a sketch directory: ")
165
- try:
166
- index = int(which) - 1
167
- args.directory = str(sketch_directories[index])
168
- except (ValueError, IndexError):
169
- print("Invalid selection.")
170
- sys.exit(1)
171
- else:
172
- print(
173
- "\nYou either need to specify a sketch directory or run in --server mode."
174
- )
175
- sys.exit(1)
176
- elif args.directory is not None and os.path.isfile(args.directory):
177
- dir_path = Path(args.directory).parent
178
- if looks_like_sketch_directory(dir_path):
179
- print(f"Using sketch directory: {dir_path}")
180
- args.directory = str(dir_path)
181
-
182
- return args
183
-
184
-
185
- def run_web_compiler(
186
- directory: Path,
187
- host: str,
188
- build_mode: BuildMode,
189
- profile: bool,
190
- last_hash_value: str | None,
191
- ) -> CompiledResult:
192
- input_dir = Path(directory)
193
- output_dir = input_dir / "fastled_js"
194
- start = time.time()
195
- web_result = web_compile(
196
- directory=input_dir, host=host, build_mode=build_mode, profile=profile
197
- )
198
- diff = time.time() - start
199
- if not web_result.success:
200
- print("\nWeb compilation failed:")
201
- print(f"Time taken: {diff:.2f} seconds")
202
- print(web_result.stdout)
203
- return CompiledResult(success=False, fastled_js="", hash_value=None)
204
-
205
- def print_results() -> None:
206
- hash_value = (
207
- web_result.hash_value
208
- if web_result.hash_value is not None
209
- else "NO HASH VALUE"
210
- )
211
- print(
212
- f"\nWeb compilation successful\n Time: {diff:.2f}\n output: {output_dir}\n hash: {hash_value}\n zip size: {len(web_result.zip_bytes)} bytes"
213
- )
214
-
215
- # now check to see if the hash value is the same as the last hash value
216
- if last_hash_value is not None and last_hash_value == web_result.hash_value:
217
- print("\nSkipping redeploy: No significant changes found.")
218
- print_results()
219
- return CompiledResult(
220
- success=True, fastled_js=str(output_dir), hash_value=web_result.hash_value
221
- )
222
-
223
- # Extract zip contents to fastled_js directory
224
- output_dir.mkdir(exist_ok=True)
225
- with tempfile.TemporaryDirectory() as temp_dir:
226
- temp_path = Path(temp_dir)
227
- temp_zip = temp_path / "result.zip"
228
- temp_zip.write_bytes(web_result.zip_bytes)
229
-
230
- # Clear existing contents
231
- shutil.rmtree(output_dir, ignore_errors=True)
232
- output_dir.mkdir(exist_ok=True)
233
-
234
- # Extract zip contents
235
- shutil.unpack_archive(temp_zip, output_dir, "zip")
236
-
237
- print(web_result.stdout)
238
- print_results()
239
- return CompiledResult(
240
- success=True, fastled_js=str(output_dir), hash_value=web_result.hash_value
241
- )
242
-
243
-
244
- def _try_start_server_or_get_url(args: argparse.Namespace) -> str | CompileServer:
245
- auto_update = args.auto_update
246
- is_local_host = "localhost" in args.web or "127.0.0.1" in args.web or args.localhost
247
- # test to see if there is already a local host server
248
- local_host_needs_server = False
249
- if is_local_host:
250
- addr = "localhost" if args.localhost else args.web
251
- urls = [addr]
252
- if ":" not in addr:
253
- urls.append(f"{addr}:{SERVER_PORT}")
254
-
255
- result: ConnectionResult | None = find_good_connection(urls)
256
- if result is not None:
257
- print(f"Found local server at {result.host}")
258
- return result.host
259
- else:
260
- local_host_needs_server = True
261
-
262
- if not local_host_needs_server and args.web:
263
- if isinstance(args.web, str):
264
- return args.web
265
- if isinstance(args.web, bool):
266
- return DEFAULT_URL
267
- return args.web
268
- else:
269
- try:
270
- print("No local server found, starting one...")
271
- compile_server = CompileServer(auto_updates=auto_update)
272
- print("Waiting for the local compiler to start...")
273
- if not compile_server.wait_for_startup():
274
- print("Failed to start local compiler.")
275
- raise RuntimeError("Failed to start local compiler.")
276
- return compile_server
277
- except KeyboardInterrupt:
278
- raise
279
- except RuntimeError:
280
- print("Failed to start local compile server, using web compiler instead.")
281
- return DEFAULT_URL
282
-
283
-
284
- def run_client(args: argparse.Namespace) -> int:
285
- compile_server: CompileServer | None = None
286
- open_web_browser = not args.just_compile and not args.interactive
287
- profile = args.profile
288
- if not args.force_compile and not looks_like_sketch_directory(Path(args.directory)):
289
- print(
290
- "Error: Not a valid FastLED sketch directory, if you are sure it is, use --force-compile"
291
- )
292
- return 1
293
-
294
- # If not explicitly using web compiler, check Docker installation
295
- if not args.web and not DockerManager.is_docker_installed():
296
- print(
297
- "\nDocker is not installed on this system - switching to web compiler instead."
298
- )
299
- args.web = True
300
-
301
- url: str
302
- try:
303
- try:
304
- url_or_server: str | CompileServer = _try_start_server_or_get_url(args)
305
- if isinstance(url_or_server, str):
306
- print(f"Found URL: {url_or_server}")
307
- url = url_or_server
308
- else:
309
- compile_server = url_or_server
310
- print(f"Server started at {compile_server.url()}")
311
- url = compile_server.url()
312
- except KeyboardInterrupt:
313
- print("\nExiting from first try...")
314
- if compile_server:
315
- compile_server.stop()
316
- return 1
317
- except Exception as e:
318
- print(f"Error: {e}")
319
- return 1
320
- build_mode: BuildMode = get_build_mode(args)
321
-
322
- def compile_function(
323
- url: str = url,
324
- build_mode: BuildMode = build_mode,
325
- profile: bool = profile,
326
- last_hash_value: str | None = None,
327
- ) -> CompiledResult:
328
- return run_web_compiler(
329
- args.directory,
330
- host=url,
331
- build_mode=build_mode,
332
- profile=profile,
333
- last_hash_value=last_hash_value,
334
- )
335
-
336
- result: CompiledResult = compile_function(last_hash_value=None)
337
- last_compiled_result: CompiledResult = result
338
-
339
- if not result.success:
340
- print("\nCompilation failed.")
341
-
342
- browser_proc: subprocess.Popen | None = None
343
- if open_web_browser:
344
- browser_proc = open_browser_thread(Path(args.directory) / "fastled_js")
345
- else:
346
- print("\nCompilation successful.")
347
- if compile_server:
348
- print("Shutting down compile server...")
349
- compile_server.stop()
350
- return 0
351
-
352
- if args.just_compile:
353
- if compile_server:
354
- compile_server.stop()
355
- if browser_proc:
356
- browser_proc.kill()
357
- return 0 if result.success else 1
358
- except KeyboardInterrupt:
359
- print("\nExiting from main")
360
- if compile_server:
361
- compile_server.stop()
362
- return 1
363
-
364
- print("\nWatching for changes. Press Ctrl+C to stop...")
365
- sketch_filewatcher = FileWatcherProcess(
366
- args.directory, excluded_patterns=["fastled_js"]
367
- )
368
-
369
- source_code_watcher: FileWatcherProcess | None = None
370
- if compile_server and compile_server.using_fastled_src_dir_volume():
371
- assert compile_server.fastled_src_dir is not None
372
- source_code_watcher = FileWatcherProcess(
373
- compile_server.fastled_src_dir, excluded_patterns=[]
374
- )
375
-
376
- def trigger_rebuild_if_sketch_changed(
377
- last_compiled_result: CompiledResult,
378
- ) -> CompiledResult:
379
- changed_files = sketch_filewatcher.get_all_changes()
380
- if changed_files:
381
- print(f"\nChanges detected in {changed_files}")
382
- last_hash_value = last_compiled_result.hash_value
383
- out = compile_function(last_hash_value=last_hash_value)
384
- if not out.success:
385
- print("\nRecompilation failed.")
386
- else:
387
- print("\nRecompilation successful.")
388
- return out
389
- return last_compiled_result
390
-
391
- try:
392
- while True:
393
- last_compiled_result = trigger_rebuild_if_sketch_changed(
394
- last_compiled_result
395
- )
396
- if compile_server and not compile_server.proceess_running():
397
- print("Server process is not running. Exiting...")
398
- return 1
399
- if source_code_watcher is not None:
400
- changed_files = source_code_watcher.get_all_changes()
401
- # de-duplicate changes
402
- changed_files = sorted(list(set(changed_files)))
403
- if changed_files:
404
- print(f"\nChanges detected in FastLED source code: {changed_files}")
405
- print("Press space bar to trigger compile.")
406
-
407
- space_key_watcher = SpaceBarWatcher()
408
- try:
409
- while True:
410
- if space_key_watcher.space_bar_pressed():
411
- print("Space bar pressed, triggering recompile...")
412
- last_compiled_result = compile_function(
413
- last_hash_value=None
414
- )
415
- print("Finished recompile.")
416
- break
417
- elif len(sketch_filewatcher.get_all_changes()) > 0:
418
- last_compiled_result = compile_function(
419
- last_hash_value=None
420
- )
421
- break
422
- time.sleep(0.1)
423
- finally:
424
- space_key_watcher.stop()
425
-
426
- except KeyboardInterrupt:
427
- print("\nStopping watch mode...")
428
- return 0
429
- except Exception as e:
430
- print(f"Error: {e}")
431
- return 1
432
- finally:
433
- sketch_filewatcher.stop()
434
- if compile_server:
435
- compile_server.stop()
436
- if browser_proc:
437
- browser_proc.kill()
438
-
439
-
440
- def run_server(args: argparse.Namespace) -> int:
441
- interactive = args.interactive
442
- auto_update = args.auto_update
443
- compile_server = CompileServer(interactive=interactive, auto_updates=auto_update)
444
- if not interactive:
445
- print(f"Server started at {compile_server.url()}")
446
- compile_server.wait_for_startup()
447
- try:
448
- while True:
449
- if not compile_server.proceess_running():
450
- print("Server process is not running. Exiting...")
451
- return 1
452
- time.sleep(1)
453
- except KeyboardInterrupt:
454
- print("\nExiting from server...")
455
- return 1
456
- finally:
457
- compile_server.stop()
458
- return 0
459
-
460
-
461
- def main() -> int:
462
- args = parse_args()
463
- if args.server:
464
- print("Running in server only mode.")
465
- return run_server(args)
466
- else:
467
- print("Running in client/server mode.")
468
- return run_client(args)
469
-
470
-
471
- if __name__ == "__main__":
472
- try:
473
- os.chdir("../fastled")
474
- sys.argv.append("--server")
475
- sys.exit(main())
476
- except KeyboardInterrupt:
477
- print("\nExiting from main...")
478
- sys.exit(1)
479
- except Exception as e:
480
- print(f"Error: {e}")
481
- sys.exit(1)
1
+ """
2
+ Uses the latest wasm compiler image to compile the FastLED sketch.
3
+ """
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+ import time
9
+ from pathlib import Path
10
+
11
+ from fastled import __version__
12
+ from fastled.client_server import run_client_server
13
+ from fastled.compile_server import CompileServer
14
+ from fastled.env import DEFAULT_URL
15
+ from fastled.select_sketch_directory import select_sketch_directory
16
+ from fastled.sketch import (
17
+ find_sketch_directories,
18
+ looks_like_fastled_repo,
19
+ looks_like_sketch_directory,
20
+ )
21
+
22
+
23
+ def parse_args() -> argparse.Namespace:
24
+ """Parse command-line arguments."""
25
+ parser = argparse.ArgumentParser(description=f"FastLED WASM Compiler {__version__}")
26
+ parser.add_argument(
27
+ "--version", action="version", version=f"%(prog)s {__version__}"
28
+ )
29
+ parser.add_argument(
30
+ "directory",
31
+ type=str,
32
+ nargs="?",
33
+ default=None,
34
+ help="Directory containing the FastLED sketch to compile",
35
+ )
36
+ parser.add_argument(
37
+ "--just-compile",
38
+ action="store_true",
39
+ help="Just compile, skip opening the browser and watching for changes.",
40
+ )
41
+ parser.add_argument(
42
+ "--web",
43
+ "-w",
44
+ type=str,
45
+ nargs="?",
46
+ # const does not seem to be working as expected
47
+ const=DEFAULT_URL, # Default value when --web is specified without value
48
+ help="Use web compiler. Optional URL can be provided (default: https://fastled.onrender.com)",
49
+ )
50
+ parser.add_argument(
51
+ "-i",
52
+ "--interactive",
53
+ action="store_true",
54
+ help="Run in interactive mode (Not available with --web)",
55
+ )
56
+ parser.add_argument(
57
+ "--profile",
58
+ action="store_true",
59
+ help="Enable profiling for web compilation",
60
+ )
61
+ parser.add_argument(
62
+ "--force-compile",
63
+ action="store_true",
64
+ help="Skips the test to see if the current directory is a valid FastLED sketch directory",
65
+ )
66
+ parser.add_argument(
67
+ "--no-auto-updates",
68
+ action="store_true",
69
+ help="Disable automatic updates of the wasm compiler image when using docker.",
70
+ )
71
+ parser.add_argument(
72
+ "--update",
73
+ action="store_true",
74
+ help="Update the wasm compiler (if necessary) before running",
75
+ )
76
+ parser.add_argument(
77
+ "--localhost",
78
+ "--local",
79
+ "-l",
80
+ action="store_true",
81
+ help="Use localhost for web compilation from an instance of fastled --server, creating it if necessary",
82
+ )
83
+ parser.add_argument(
84
+ "--server",
85
+ "-s",
86
+ action="store_true",
87
+ help="Run the server in the current directory, volume mapping fastled if we are in the repo",
88
+ )
89
+ build_mode = parser.add_mutually_exclusive_group()
90
+ build_mode.add_argument("--debug", action="store_true", help="Build in debug mode")
91
+ build_mode.add_argument(
92
+ "--quick",
93
+ action="store_true",
94
+ default=True,
95
+ help="Build in quick mode (default)",
96
+ )
97
+ build_mode.add_argument(
98
+ "--release", action="store_true", help="Build in release mode"
99
+ )
100
+
101
+ cwd_is_fastled = looks_like_fastled_repo(Path(os.getcwd()))
102
+
103
+ args = parser.parse_args()
104
+ if args.update:
105
+ args.auto_update = True
106
+ elif args.no_auto_updates:
107
+ args.auto_update = False
108
+ else:
109
+ args.auto_update = None
110
+
111
+ if not cwd_is_fastled and not args.localhost and not args.web and not args.server:
112
+ print(f"Using web compiler at {DEFAULT_URL}")
113
+ args.web = DEFAULT_URL
114
+ if cwd_is_fastled and not args.web and not args.server:
115
+ print("Forcing --local mode because we are in the FastLED repo")
116
+ args.localhost = True
117
+ if args.localhost:
118
+ args.web = "localhost"
119
+ if args.interactive and not args.server:
120
+ print("--interactive forces --server mode")
121
+ args.server = True
122
+ if args.directory is None and not args.server:
123
+ # does current directory look like a sketch?
124
+ maybe_sketch_dir = Path(os.getcwd())
125
+ if looks_like_sketch_directory(maybe_sketch_dir):
126
+ args.directory = str(maybe_sketch_dir)
127
+ else:
128
+ sketch_directories = find_sketch_directories(maybe_sketch_dir)
129
+ selected_dir = select_sketch_directory(sketch_directories, cwd_is_fastled)
130
+ if selected_dir:
131
+ print(f"Using sketch directory: {selected_dir}")
132
+ args.directory = selected_dir
133
+ else:
134
+ print(
135
+ "\nYou either need to specify a sketch directory or run in --server mode."
136
+ )
137
+ sys.exit(1)
138
+ elif args.directory is not None and os.path.isfile(args.directory):
139
+ dir_path = Path(args.directory).parent
140
+ if looks_like_sketch_directory(dir_path):
141
+ print(f"Using sketch directory: {dir_path}")
142
+ args.directory = str(dir_path)
143
+
144
+ return args
145
+
146
+
147
+ def run_server(args: argparse.Namespace) -> int:
148
+ interactive = args.interactive
149
+ auto_update = args.auto_update
150
+ compile_server = CompileServer(interactive=interactive, auto_updates=auto_update)
151
+ if not interactive:
152
+ print(f"Server started at {compile_server.url()}")
153
+ compile_server.wait_for_startup()
154
+ try:
155
+ while True:
156
+ if not compile_server.proceess_running():
157
+ print("Server process is not running. Exiting...")
158
+ return 1
159
+ time.sleep(1)
160
+ except KeyboardInterrupt:
161
+ print("\nExiting from server...")
162
+ return 1
163
+ finally:
164
+ compile_server.stop()
165
+ return 0
166
+
167
+
168
+ def main() -> int:
169
+ args = parse_args()
170
+ if args.server:
171
+ print("Running in server only mode.")
172
+ return run_server(args)
173
+ else:
174
+ print("Running in client/server mode.")
175
+ return run_client_server(args)
176
+
177
+
178
+ if __name__ == "__main__":
179
+ try:
180
+ os.chdir("../fastled")
181
+ sys.exit(main())
182
+ except KeyboardInterrupt:
183
+ print("\nExiting from main...")
184
+ sys.exit(1)
185
+ except Exception as e:
186
+ print(f"Error: {e}")
187
+ sys.exit(1)
@@ -0,0 +1,299 @@
1
+ import argparse
2
+ import shutil
3
+ import subprocess
4
+ import tempfile
5
+ import time
6
+ from pathlib import Path
7
+
8
+ from fastled.build_mode import BuildMode, get_build_mode
9
+ from fastled.compile_server import CompileServer
10
+ from fastled.docker_manager import DockerManager
11
+ from fastled.env import DEFAULT_URL
12
+ from fastled.filewatcher import FileWatcherProcess
13
+ from fastled.keyboard import SpaceBarWatcher
14
+ from fastled.open_browser import open_browser_thread
15
+ from fastled.sketch import looks_like_sketch_directory
16
+
17
+ # CompiledResult
18
+ from fastled.types import CompiledResult
19
+ from fastled.web_compile import (
20
+ SERVER_PORT,
21
+ ConnectionResult,
22
+ find_good_connection,
23
+ web_compile,
24
+ )
25
+
26
+
27
+ def _run_web_compiler(
28
+ directory: Path,
29
+ host: str,
30
+ build_mode: BuildMode,
31
+ profile: bool,
32
+ last_hash_value: str | None,
33
+ ) -> CompiledResult:
34
+ input_dir = Path(directory)
35
+ output_dir = input_dir / "fastled_js"
36
+ start = time.time()
37
+ web_result = web_compile(
38
+ directory=input_dir, host=host, build_mode=build_mode, profile=profile
39
+ )
40
+ diff = time.time() - start
41
+ if not web_result.success:
42
+ print("\nWeb compilation failed:")
43
+ print(f"Time taken: {diff:.2f} seconds")
44
+ print(web_result.stdout)
45
+ return CompiledResult(success=False, fastled_js="", hash_value=None)
46
+
47
+ def print_results() -> None:
48
+ hash_value = (
49
+ web_result.hash_value
50
+ if web_result.hash_value is not None
51
+ else "NO HASH VALUE"
52
+ )
53
+ print(
54
+ f"\nWeb compilation successful\n Time: {diff:.2f}\n output: {output_dir}\n hash: {hash_value}\n zip size: {len(web_result.zip_bytes)} bytes"
55
+ )
56
+
57
+ # now check to see if the hash value is the same as the last hash value
58
+ if last_hash_value is not None and last_hash_value == web_result.hash_value:
59
+ print("\nSkipping redeploy: No significant changes found.")
60
+ print_results()
61
+ return CompiledResult(
62
+ success=True, fastled_js=str(output_dir), hash_value=web_result.hash_value
63
+ )
64
+
65
+ # Extract zip contents to fastled_js directory
66
+ output_dir.mkdir(exist_ok=True)
67
+ with tempfile.TemporaryDirectory() as temp_dir:
68
+ temp_path = Path(temp_dir)
69
+ temp_zip = temp_path / "result.zip"
70
+ temp_zip.write_bytes(web_result.zip_bytes)
71
+
72
+ # Clear existing contents
73
+ shutil.rmtree(output_dir, ignore_errors=True)
74
+ output_dir.mkdir(exist_ok=True)
75
+
76
+ # Extract zip contents
77
+ shutil.unpack_archive(temp_zip, output_dir, "zip")
78
+
79
+ print(web_result.stdout)
80
+ print_results()
81
+ return CompiledResult(
82
+ success=True, fastled_js=str(output_dir), hash_value=web_result.hash_value
83
+ )
84
+
85
+
86
+ def _try_start_server_or_get_url(args: argparse.Namespace) -> str | CompileServer:
87
+ auto_update = args.auto_update
88
+ is_local_host = "localhost" in args.web or "127.0.0.1" in args.web or args.localhost
89
+ # test to see if there is already a local host server
90
+ local_host_needs_server = False
91
+ if is_local_host:
92
+ addr = "localhost" if args.localhost else args.web
93
+ urls = [addr]
94
+ if ":" not in addr:
95
+ urls.append(f"{addr}:{SERVER_PORT}")
96
+
97
+ result: ConnectionResult | None = find_good_connection(urls)
98
+ if result is not None:
99
+ print(f"Found local server at {result.host}")
100
+ return result.host
101
+ else:
102
+ local_host_needs_server = True
103
+
104
+ if not local_host_needs_server and args.web:
105
+ if isinstance(args.web, str):
106
+ return args.web
107
+ if isinstance(args.web, bool):
108
+ return DEFAULT_URL
109
+ return args.web
110
+ else:
111
+ try:
112
+ print("No local server found, starting one...")
113
+ compile_server = CompileServer(auto_updates=auto_update)
114
+ print("Waiting for the local compiler to start...")
115
+ if not compile_server.wait_for_startup():
116
+ print("Failed to start local compiler.")
117
+ raise RuntimeError("Failed to start local compiler.")
118
+ return compile_server
119
+ except KeyboardInterrupt:
120
+ raise
121
+ except RuntimeError:
122
+ print("Failed to start local compile server, using web compiler instead.")
123
+ return DEFAULT_URL
124
+
125
+
126
+ def run_client_server(args: argparse.Namespace) -> int:
127
+ compile_server: CompileServer | None = None
128
+ open_web_browser = not args.just_compile and not args.interactive
129
+ profile = args.profile
130
+ if not args.force_compile and not looks_like_sketch_directory(Path(args.directory)):
131
+ print(
132
+ "Error: Not a valid FastLED sketch directory, if you are sure it is, use --force-compile"
133
+ )
134
+ return 1
135
+
136
+ # If not explicitly using web compiler, check Docker installation
137
+ if not args.web and not DockerManager.is_docker_installed():
138
+ print(
139
+ "\nDocker is not installed on this system - switching to web compiler instead."
140
+ )
141
+ args.web = True
142
+
143
+ url: str
144
+ try:
145
+ try:
146
+ url_or_server: str | CompileServer = _try_start_server_or_get_url(args)
147
+ if isinstance(url_or_server, str):
148
+ print(f"Found URL: {url_or_server}")
149
+ url = url_or_server
150
+ else:
151
+ compile_server = url_or_server
152
+ print(f"Server started at {compile_server.url()}")
153
+ url = compile_server.url()
154
+ except KeyboardInterrupt:
155
+ print("\nExiting from first try...")
156
+ if compile_server:
157
+ compile_server.stop()
158
+ return 1
159
+ except Exception as e:
160
+ print(f"Error: {e}")
161
+ return 1
162
+ build_mode: BuildMode = get_build_mode(args)
163
+
164
+ def compile_function(
165
+ url: str = url,
166
+ build_mode: BuildMode = build_mode,
167
+ profile: bool = profile,
168
+ last_hash_value: str | None = None,
169
+ ) -> CompiledResult:
170
+ return _run_web_compiler(
171
+ args.directory,
172
+ host=url,
173
+ build_mode=build_mode,
174
+ profile=profile,
175
+ last_hash_value=last_hash_value,
176
+ )
177
+
178
+ result: CompiledResult = compile_function(last_hash_value=None)
179
+ last_compiled_result: CompiledResult = result
180
+
181
+ if not result.success:
182
+ print("\nCompilation failed.")
183
+
184
+ browser_proc: subprocess.Popen | None = None
185
+ if open_web_browser:
186
+ browser_proc = open_browser_thread(Path(args.directory) / "fastled_js")
187
+ else:
188
+ print("\nCompilation successful.")
189
+ if compile_server:
190
+ print("Shutting down compile server...")
191
+ compile_server.stop()
192
+ return 0
193
+
194
+ if args.just_compile:
195
+ if compile_server:
196
+ compile_server.stop()
197
+ if browser_proc:
198
+ browser_proc.kill()
199
+ return 0 if result.success else 1
200
+ except KeyboardInterrupt:
201
+ print("\nExiting from main")
202
+ if compile_server:
203
+ compile_server.stop()
204
+ return 1
205
+
206
+ sketch_filewatcher = FileWatcherProcess(
207
+ args.directory, excluded_patterns=["fastled_js"]
208
+ )
209
+
210
+ source_code_watcher: FileWatcherProcess | None = None
211
+ if compile_server and compile_server.using_fastled_src_dir_volume():
212
+ assert compile_server.fastled_src_dir is not None
213
+ source_code_watcher = FileWatcherProcess(
214
+ compile_server.fastled_src_dir, excluded_patterns=[]
215
+ )
216
+
217
+ def trigger_rebuild_if_sketch_changed(
218
+ last_compiled_result: CompiledResult,
219
+ ) -> tuple[bool, CompiledResult]:
220
+ changed_files = sketch_filewatcher.get_all_changes()
221
+ if changed_files:
222
+ print(f"\nChanges detected in {changed_files}")
223
+ last_hash_value = last_compiled_result.hash_value
224
+ out = compile_function(last_hash_value=last_hash_value)
225
+ if not out.success:
226
+ print("\nRecompilation failed.")
227
+ else:
228
+ print("\nRecompilation successful.")
229
+ return True, out
230
+ return False, last_compiled_result
231
+
232
+ def print_status() -> None:
233
+ print("Will compile on sketch changes or if you hit the space bar.")
234
+
235
+ print_status()
236
+ print("Press Ctrl+C to stop...")
237
+
238
+ try:
239
+ while True:
240
+ if SpaceBarWatcher.watch_space_bar_pressed(timeout=1.0):
241
+ print("Compiling...")
242
+ last_compiled_result = compile_function(last_hash_value=None)
243
+ if not last_compiled_result.success:
244
+ print("\nRecompilation failed.")
245
+ else:
246
+ print("\nRecompilation successful.")
247
+ # drain the space bar queue
248
+ SpaceBarWatcher.watch_space_bar_pressed()
249
+ print_status()
250
+ continue
251
+ changed, last_compiled_result = trigger_rebuild_if_sketch_changed(
252
+ last_compiled_result
253
+ )
254
+ if changed:
255
+ print_status()
256
+ continue
257
+ if compile_server and not compile_server.proceess_running():
258
+ print("Server process is not running. Exiting...")
259
+ return 1
260
+ if source_code_watcher is not None:
261
+ changed_files = source_code_watcher.get_all_changes()
262
+ # de-duplicate changes
263
+ changed_files = sorted(list(set(changed_files)))
264
+ if changed_files:
265
+ print(f"\nChanges detected in FastLED source code: {changed_files}")
266
+ print("Press space bar to trigger compile.")
267
+ while True:
268
+ space_bar_pressed = SpaceBarWatcher.watch_space_bar_pressed(
269
+ timeout=1.0
270
+ )
271
+ file_had_changes = (
272
+ len(source_code_watcher.get_all_changes()) > 0
273
+ )
274
+ if space_bar_pressed or file_had_changes:
275
+ if space_bar_pressed:
276
+ print("Space bar pressed, triggering recompile...")
277
+ elif file_had_changes:
278
+ print("Changes detected, triggering recompile...")
279
+ last_compiled_result = compile_function(
280
+ last_hash_value=None
281
+ )
282
+ print("Finished recompile.")
283
+ # Drain the space bar queue
284
+ SpaceBarWatcher.watch_space_bar_pressed()
285
+ print_status()
286
+ continue
287
+
288
+ except KeyboardInterrupt:
289
+ print("\nStopping watch mode...")
290
+ return 0
291
+ except Exception as e:
292
+ print(f"Error: {e}")
293
+ return 1
294
+ finally:
295
+ sketch_filewatcher.stop()
296
+ if compile_server:
297
+ compile_server.stop()
298
+ if browser_proc:
299
+ browser_proc.kill()
fastled/env.py ADDED
@@ -0,0 +1,8 @@
1
+ import os
2
+ import platform
3
+
4
+ MACHINE = platform.machine().lower()
5
+ IS_ARM: bool = "arm" in MACHINE or "aarch64" in MACHINE
6
+ PLATFORM_TAG: str = "-arm64" if IS_ARM else ""
7
+ CONTAINER_NAME = f"fastled-wasm-compiler{PLATFORM_TAG}"
8
+ DEFAULT_URL = str(os.environ.get("FASTLED_URL", "https://fastled.onrender.com"))
fastled/keyboard.py CHANGED
@@ -5,8 +5,25 @@ import time
5
5
  from multiprocessing import Process, Queue
6
6
  from queue import Empty
7
7
 
8
+ _WHITE_SPACE = [" ", "\r", "\n"]
8
9
 
10
+
11
+ # Original space bar, but now also enter key.
9
12
  class SpaceBarWatcher:
13
+
14
+ @classmethod
15
+ def watch_space_bar_pressed(cls, timeout: float = 0) -> bool:
16
+ watcher = cls()
17
+ try:
18
+ start_time = time.time()
19
+ while True:
20
+ if watcher.space_bar_pressed():
21
+ return True
22
+ if time.time() - start_time > timeout:
23
+ return False
24
+ finally:
25
+ watcher.stop()
26
+
10
27
  def __init__(self) -> None:
11
28
  self.queue: Queue = Queue()
12
29
  self.queue_cancel: Queue = Queue()
@@ -31,7 +48,7 @@ class SpaceBarWatcher:
31
48
  # Check if there's input ready
32
49
  if msvcrt.kbhit(): # type: ignore
33
50
  char = msvcrt.getch().decode() # type: ignore
34
- if char == " ":
51
+ if char in _WHITE_SPACE:
35
52
  self.queue.put(ord(" "))
36
53
 
37
54
  else: # Unix-like systems
@@ -52,7 +69,7 @@ class SpaceBarWatcher:
52
69
  # Check if there's input ready
53
70
  if select.select([sys.stdin], [], [], 0.1)[0]:
54
71
  char = sys.stdin.read(1)
55
- if char == " ":
72
+ if char in _WHITE_SPACE:
56
73
  self.queue.put(ord(" "))
57
74
  finally:
58
75
  termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) # type: ignore
@@ -60,9 +77,12 @@ class SpaceBarWatcher:
60
77
  def space_bar_pressed(self) -> bool:
61
78
  found = False
62
79
  while not self.queue.empty():
63
- key = self.queue.get()
64
- if key == ord(" "):
65
- found = True
80
+ try:
81
+ key = self.queue.get(block=False, timeout=0.1)
82
+ if key == ord(" "):
83
+ found = True
84
+ except Empty:
85
+ break
66
86
  return found
67
87
 
68
88
  def stop(self) -> None:
@@ -0,0 +1,35 @@
1
+ from pathlib import Path
2
+
3
+ from fastled.string_diff import string_diff_paths
4
+
5
+
6
+ def select_sketch_directory(
7
+ sketch_directories: list[Path], cwd_is_fastled: bool
8
+ ) -> str | None:
9
+ if cwd_is_fastled:
10
+ exclude = ["src", "dev", "tests"]
11
+ for ex in exclude:
12
+ p = Path(ex)
13
+ if p in sketch_directories:
14
+ sketch_directories.remove(p)
15
+
16
+ if len(sketch_directories) == 1:
17
+ print(f"\nUsing sketch directory: {sketch_directories[0]}")
18
+ return str(sketch_directories[0])
19
+ elif len(sketch_directories) > 1:
20
+ print("\nMultiple Directories found, choose one:")
21
+ for i, sketch_dir in enumerate(sketch_directories):
22
+ print(f" [{i+1}]: {sketch_dir}")
23
+ which = input("\nPlease specify a sketch directory: ")
24
+ try:
25
+ index = int(which) - 1
26
+ return str(sketch_directories[index])
27
+ except (ValueError, IndexError):
28
+ inputs = [p for p in sketch_directories]
29
+ top_hits: list[tuple[int, Path]] = string_diff_paths(which, inputs)
30
+ if len(top_hits) == 1:
31
+ example = top_hits[0][1]
32
+ return str(example)
33
+ else:
34
+ return select_sketch_directory([p for _, p in top_hits], cwd_is_fastled)
35
+ return None
fastled/string_diff.py ADDED
@@ -0,0 +1,37 @@
1
+ from pathlib import Path
2
+
3
+ from rapidfuzz.distance import Levenshtein
4
+
5
+
6
+ # Returns the min distance strings. If there is a tie, it returns
7
+ # all the strings that have the same min distance.
8
+ # Returns a tuple of index and string.
9
+ def string_diff(
10
+ input_string: str, string_list: list[str], ignore_case=True
11
+ ) -> list[tuple[int, str]]:
12
+
13
+ def normalize(s: str) -> str:
14
+ return s.lower() if ignore_case else s
15
+
16
+ distances = [
17
+ Levenshtein.distance(normalize(input_string), normalize(s)) for s in string_list
18
+ ]
19
+ min_distance = min(distances)
20
+ out: list[tuple[int, str]] = []
21
+ for i, d in enumerate(distances):
22
+ if d == min_distance:
23
+ out.append((i, string_list[i]))
24
+ return out
25
+
26
+
27
+ def string_diff_paths(
28
+ input_string: str | Path, path_list: list[Path], ignore_case=True
29
+ ) -> list[tuple[int, Path]]:
30
+ string_list = [str(p) for p in path_list]
31
+ tmp = string_diff(str(input_string), string_list, ignore_case)
32
+ # out: list[tuple[int, Path]] = [(i, Path(path_list[j])) for i, j in tmp]
33
+ out: list[tuple[int, Path]] = []
34
+ for i, j in tmp:
35
+ p = Path(j)
36
+ out.append((i, p))
37
+ return out
fastled/types.py ADDED
@@ -0,0 +1,10 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class CompiledResult:
6
+ """Dataclass to hold the result of the compilation."""
7
+
8
+ success: bool
9
+ fastled_js: str
10
+ hash_value: str | None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastled
3
- Version: 1.1.20
3
+ Version: 1.1.22
4
4
  Summary: FastLED Wasm Compiler
5
5
  Home-page: https://github.com/zackees/fastled-wasm
6
6
  Maintainer: Zachary Vorhies
@@ -18,6 +18,7 @@ Requires-Dist: download
18
18
  Requires-Dist: filelock
19
19
  Requires-Dist: disklru>=2.0.1
20
20
  Requires-Dist: appdirs
21
+ Requires-Dist: rapidfuzz
21
22
 
22
23
  # FastLED Wasm compiler
23
24
 
@@ -161,6 +162,8 @@ A: A big chunk of space is being used by unnecessary javascript `emscripten` is
161
162
 
162
163
  # Revisions
163
164
 
165
+ * 1.1.22 - Selecting sketch now allows strings and narrowing down paths if ambiguity
166
+ * 1.1.21 - Now always watches for space/enter key events to trigger a recompile.
164
167
  * 1.1.20 - Fixed a regression for 1.1.16 involving docker throwing an exception before DockerManager.is_running() could be called so it can be launched.
165
168
  * 1.1.19 - Automatically does a limit searches for sketch directories if you leave it blank.
166
169
  * 1.1.18 - Fixes for when the image has never been downloaded.
@@ -0,0 +1,25 @@
1
+ fastled/__init__.py,sha256=Vwrw9C5ep3js4u63kaxEdkec1V40HfactjPKA7tUZv4,64
2
+ fastled/app.py,sha256=UXHR4nFJzdU-3WGk9J7BB0RxkQVwKZ7XKCgfLp2yqW0,6281
3
+ fastled/build_mode.py,sha256=joMwsV4K1y_LijT4gEAcjx69RZBoe_KmFmHZdPYbL_4,631
4
+ fastled/cli.py,sha256=CNR_pQR0sNVPNuv8e_nmm-0PI8sU-eUBUgnWgWkzW9c,237
5
+ fastled/client_server.py,sha256=TLEmCxafrcBzo9FAz3OlIIgsSbNpVGsSuvZnIXpHXBA,11090
6
+ fastled/compile_server.py,sha256=aBdpILSRrDsCJ5e9g5uwIqt9bcqE_8FrSddCV2ygtrI,5401
7
+ fastled/docker_manager.py,sha256=5IXaLt8k4WSSiWmvqMvwMYKngAgZ-t2p2deJ6cmJNwk,20434
8
+ fastled/env.py,sha256=8wctQwl5qE4CI8NBugHtgMmUfEfHZ869JX5lGdSOJxc,304
9
+ fastled/filewatcher.py,sha256=5dVmjEG23kMeJa29tRVm5XKSr9sTD4ME2boo-CFDuUM,6910
10
+ fastled/keyboard.py,sha256=TkFjDxGtGdDbjQF_LRqp54hmdcKaXjf1olTT8l6dEJk,3170
11
+ fastled/open_browser.py,sha256=RRHcsZ5Vzsw1AuZUEYuSfjKmf_9j3NGMDUR-FndHmqs,1483
12
+ fastled/paths.py,sha256=VsPmgu0lNSCFOoEC0BsTYzDygXqy15AHUfN-tTuzDZA,99
13
+ fastled/select_sketch_directory.py,sha256=S9h8pHCDshaUGHdXaaFoh8g189ewA-GbKVEHKyaO2iQ,1304
14
+ fastled/sketch.py,sha256=5nRjg281lMH8Bo9wKjbcpTQCfEP574ZCG-lukvFmyQ8,2656
15
+ fastled/string_diff.py,sha256=svtaQFGp4a6r2Qjx-Gxhna94wK-d8LKV4OCpKMXiHso,1164
16
+ fastled/types.py,sha256=dDIsGHJkHNJ7B61wNp6X0JSLs_nrHiq7RlNqNWbwFec,194
17
+ fastled/util.py,sha256=t4M3NFMhnCzfYbLvIyJi0RdFssZqbTN_vVIaej1WV-U,265
18
+ fastled/web_compile.py,sha256=KuvKGdX6SSUUqC7YgX4T9SMSP5wdcPUhpg9-K9zPoTI,10378
19
+ fastled/assets/example.txt,sha256=lTBovRjiz0_TgtAtbA1C5hNi2ffbqnNPqkKg6UiKCT8,54
20
+ fastled-1.1.22.dist-info/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
21
+ fastled-1.1.22.dist-info/METADATA,sha256=Xr6-uF2ohZ8Jjg3FSxLojs5nK6w7nbJ_cZ_e_1eGVeY,14012
22
+ fastled-1.1.22.dist-info/WHEEL,sha256=0VNUDWQJzfRahYI3neAhz2UVbRCtztpN5dPHAGvmGXc,109
23
+ fastled-1.1.22.dist-info/entry_points.txt,sha256=RCwmzCSOS4-C2i9EziANq7Z2Zb4KFnEMR1FQC0bBwAw,101
24
+ fastled-1.1.22.dist-info/top_level.txt,sha256=xfG6Z_ol9V5YmBROkZq2QTRwjbS2ouCUxaTJsOwfkOo,14
25
+ fastled-1.1.22.dist-info/RECORD,,
@@ -1,20 +0,0 @@
1
- fastled/__init__.py,sha256=p51kZEN1JznXCt_KUL-m7ZWLsSop3-7JuCT0ywmQs3A,64
2
- fastled/app.py,sha256=gLmma0cWlXgkCGPLt3DQIUAZuU3EdxfzNZfAi0ghEOI,16882
3
- fastled/build_mode.py,sha256=joMwsV4K1y_LijT4gEAcjx69RZBoe_KmFmHZdPYbL_4,631
4
- fastled/cli.py,sha256=CNR_pQR0sNVPNuv8e_nmm-0PI8sU-eUBUgnWgWkzW9c,237
5
- fastled/compile_server.py,sha256=aBdpILSRrDsCJ5e9g5uwIqt9bcqE_8FrSddCV2ygtrI,5401
6
- fastled/docker_manager.py,sha256=5IXaLt8k4WSSiWmvqMvwMYKngAgZ-t2p2deJ6cmJNwk,20434
7
- fastled/filewatcher.py,sha256=5dVmjEG23kMeJa29tRVm5XKSr9sTD4ME2boo-CFDuUM,6910
8
- fastled/keyboard.py,sha256=rqndglWYzRy6oiqHgsmx1peLd0Yrpci01zGENlCzh_s,2576
9
- fastled/open_browser.py,sha256=RRHcsZ5Vzsw1AuZUEYuSfjKmf_9j3NGMDUR-FndHmqs,1483
10
- fastled/paths.py,sha256=VsPmgu0lNSCFOoEC0BsTYzDygXqy15AHUfN-tTuzDZA,99
11
- fastled/sketch.py,sha256=5nRjg281lMH8Bo9wKjbcpTQCfEP574ZCG-lukvFmyQ8,2656
12
- fastled/util.py,sha256=t4M3NFMhnCzfYbLvIyJi0RdFssZqbTN_vVIaej1WV-U,265
13
- fastled/web_compile.py,sha256=KuvKGdX6SSUUqC7YgX4T9SMSP5wdcPUhpg9-K9zPoTI,10378
14
- fastled/assets/example.txt,sha256=lTBovRjiz0_TgtAtbA1C5hNi2ffbqnNPqkKg6UiKCT8,54
15
- fastled-1.1.20.dist-info/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
16
- fastled-1.1.20.dist-info/METADATA,sha256=XyDrNrK_WIiy21Xj_xlIWUgfdMiZ0KGDnasYL-or2Xg,13814
17
- fastled-1.1.20.dist-info/WHEEL,sha256=0VNUDWQJzfRahYI3neAhz2UVbRCtztpN5dPHAGvmGXc,109
18
- fastled-1.1.20.dist-info/entry_points.txt,sha256=RCwmzCSOS4-C2i9EziANq7Z2Zb4KFnEMR1FQC0bBwAw,101
19
- fastled-1.1.20.dist-info/top_level.txt,sha256=xfG6Z_ol9V5YmBROkZq2QTRwjbS2ouCUxaTJsOwfkOo,14
20
- fastled-1.1.20.dist-info/RECORD,,