fastled 1.4.41__tar.gz → 1.4.45__tar.gz

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 (150) hide show
  1. {fastled-1.4.41 → fastled-1.4.45}/PKG-INFO +1 -1
  2. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/__version__.py +1 -1
  3. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/docker_manager.py +35 -19
  4. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/project_init.py +20 -13
  5. fastled-1.4.45/src/fastled/select_sketch_directory.py +122 -0
  6. {fastled-1.4.41 → fastled-1.4.45}/src/fastled.egg-info/PKG-INFO +1 -1
  7. {fastled-1.4.41 → fastled-1.4.45}/src/fastled.egg-info/SOURCES.txt +1 -0
  8. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_manual_api_invocation.py +6 -3
  9. fastled-1.4.45/tests/unit/test_select_sketch_directory.py +103 -0
  10. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_string_diff_comprehensive.py +36 -0
  11. fastled-1.4.41/src/fastled/select_sketch_directory.py +0 -67
  12. {fastled-1.4.41 → fastled-1.4.45}/.aiderignore +0 -0
  13. {fastled-1.4.41 → fastled-1.4.45}/.claude/settings.local.json +0 -0
  14. {fastled-1.4.41 → fastled-1.4.45}/.cursorrules +0 -0
  15. {fastled-1.4.41 → fastled-1.4.45}/.dockerignore +0 -0
  16. {fastled-1.4.41 → fastled-1.4.45}/.github/workflows/build_multi_docker_image.yml +0 -0
  17. {fastled-1.4.41 → fastled-1.4.45}/.github/workflows/build_webpage.yml +0 -0
  18. {fastled-1.4.41 → fastled-1.4.45}/.github/workflows/lint.yml +0 -0
  19. {fastled-1.4.41 → fastled-1.4.45}/.github/workflows/publish_release.yml +0 -0
  20. {fastled-1.4.41 → fastled-1.4.45}/.github/workflows/template_build_docker_image.yml +0 -0
  21. {fastled-1.4.41 → fastled-1.4.45}/.github/workflows/test_build_exe.yml +0 -0
  22. {fastled-1.4.41 → fastled-1.4.45}/.github/workflows/test_macos.yml +0 -0
  23. {fastled-1.4.41 → fastled-1.4.45}/.github/workflows/test_ubuntu.yml +0 -0
  24. {fastled-1.4.41 → fastled-1.4.45}/.github/workflows/test_win.yml +0 -0
  25. {fastled-1.4.41 → fastled-1.4.45}/.gitignore +0 -0
  26. {fastled-1.4.41 → fastled-1.4.45}/.pylintrc +0 -0
  27. {fastled-1.4.41 → fastled-1.4.45}/.vscode/launch.json +0 -0
  28. {fastled-1.4.41 → fastled-1.4.45}/.vscode/settings.json +0 -0
  29. {fastled-1.4.41 → fastled-1.4.45}/.vscode/tasks.json +0 -0
  30. {fastled-1.4.41 → fastled-1.4.45}/DEBUGGER.md +0 -0
  31. {fastled-1.4.41 → fastled-1.4.45}/Dockerfile +0 -0
  32. {fastled-1.4.41 → fastled-1.4.45}/EXTENDED_INO.md +0 -0
  33. {fastled-1.4.41 → fastled-1.4.45}/FAQ.md +0 -0
  34. {fastled-1.4.41 → fastled-1.4.45}/LICENSE +0 -0
  35. {fastled-1.4.41 → fastled-1.4.45}/MANIFEST.in +0 -0
  36. {fastled-1.4.41 → fastled-1.4.45}/README.md +0 -0
  37. {fastled-1.4.41 → fastled-1.4.45}/RELEASE.md +0 -0
  38. {fastled-1.4.41 → fastled-1.4.45}/TODO.md +0 -0
  39. {fastled-1.4.41 → fastled-1.4.45}/build_exe.py +0 -0
  40. {fastled-1.4.41 → fastled-1.4.45}/build_local_docker.py +0 -0
  41. {fastled-1.4.41 → fastled-1.4.45}/build_site.py +0 -0
  42. {fastled-1.4.41 → fastled-1.4.45}/chrome_vscode_bridge_design_task.md +0 -0
  43. {fastled-1.4.41 → fastled-1.4.45}/clean +0 -0
  44. {fastled-1.4.41 → fastled-1.4.45}/compiler/debug.sh +0 -0
  45. {fastled-1.4.41 → fastled-1.4.45}/compiler/run.py +0 -0
  46. {fastled-1.4.41 → fastled-1.4.45}/demo/100dots.html +0 -0
  47. {fastled-1.4.41 → fastled-1.4.45}/demo/demo_threejs.html +0 -0
  48. {fastled-1.4.41 → fastled-1.4.45}/demo/micdemo.html +0 -0
  49. {fastled-1.4.41 → fastled-1.4.45}/demo/mp3upload.html +0 -0
  50. {fastled-1.4.41 → fastled-1.4.45}/demo/webgl_postprocessing_unreal_bloom.html +0 -0
  51. {fastled-1.4.41 → fastled-1.4.45}/docker-compose.yml +0 -0
  52. {fastled-1.4.41 → fastled-1.4.45}/entrypoint.sh +0 -0
  53. {fastled-1.4.41 → fastled-1.4.45}/install +0 -0
  54. {fastled-1.4.41 → fastled-1.4.45}/install_linux.sh +0 -0
  55. {fastled-1.4.41 → fastled-1.4.45}/lint +0 -0
  56. {fastled-1.4.41 → fastled-1.4.45}/pyproject.toml +0 -0
  57. {fastled-1.4.41 → fastled-1.4.45}/requirements.docker.txt +0 -0
  58. {fastled-1.4.41 → fastled-1.4.45}/requirements.testing.txt +0 -0
  59. {fastled-1.4.41 → fastled-1.4.45}/setup.cfg +0 -0
  60. {fastled-1.4.41 → fastled-1.4.45}/setup.py +0 -0
  61. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/__init__.py +0 -0
  62. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/__main__.py +0 -0
  63. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/app.py +0 -0
  64. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/args.py +0 -0
  65. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/assets/example.txt +0 -0
  66. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/assets/localhost-key.pem +0 -0
  67. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/assets/localhost.pem +0 -0
  68. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/cli.py +0 -0
  69. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/cli_test.py +0 -0
  70. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/cli_test_interactive.py +0 -0
  71. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/client_server.py +0 -0
  72. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/compile_server.py +0 -0
  73. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/compile_server_impl.py +0 -0
  74. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/emoji_util.py +0 -0
  75. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/filewatcher.py +0 -0
  76. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/find_good_connection.py +0 -0
  77. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/header_dump.py +0 -0
  78. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/install/__init__.py +0 -0
  79. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/install/examples_manager.py +0 -0
  80. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/install/extension_manager.py +0 -0
  81. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/install/main.py +0 -0
  82. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/install/project_detection.py +0 -0
  83. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/install/test_install.py +0 -0
  84. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/install/vscode_config.py +0 -0
  85. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/interruptible_http.py +0 -0
  86. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/keyboard.py +0 -0
  87. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/keyz.py +0 -0
  88. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/live_client.py +0 -0
  89. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/open_browser.py +0 -0
  90. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/parse_args.py +0 -0
  91. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/paths.py +0 -0
  92. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/playwright/chrome_extension_downloader.py +0 -0
  93. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/playwright/playwright_browser.py +0 -0
  94. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/playwright/resize_tracking.py +0 -0
  95. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/print_filter.py +0 -0
  96. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/server_flask.py +0 -0
  97. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/server_start.py +0 -0
  98. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/settings.py +0 -0
  99. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/site/build.py +0 -0
  100. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/site/examples.py +0 -0
  101. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/sketch.py +0 -0
  102. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/spinner.py +0 -0
  103. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/string_diff.py +0 -0
  104. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/test/can_run_local_docker_tests.py +0 -0
  105. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/test/examples.py +0 -0
  106. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/types.py +0 -0
  107. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/util.py +0 -0
  108. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/version.py +0 -0
  109. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/web_compile.py +0 -0
  110. {fastled-1.4.41 → fastled-1.4.45}/src/fastled/zip_files.py +0 -0
  111. {fastled-1.4.41 → fastled-1.4.45}/src/fastled.egg-info/dependency_links.txt +0 -0
  112. {fastled-1.4.41 → fastled-1.4.45}/src/fastled.egg-info/entry_points.txt +0 -0
  113. {fastled-1.4.41 → fastled-1.4.45}/src/fastled.egg-info/requires.txt +0 -0
  114. {fastled-1.4.41 → fastled-1.4.45}/src/fastled.egg-info/top_level.txt +0 -0
  115. {fastled-1.4.41 → fastled-1.4.45}/task.md +0 -0
  116. {fastled-1.4.41 → fastled-1.4.45}/task2.md +0 -0
  117. {fastled-1.4.41 → fastled-1.4.45}/test +0 -0
  118. {fastled-1.4.41 → fastled-1.4.45}/tests/integration/test_build_examples.py +0 -0
  119. {fastled-1.4.41 → fastled-1.4.45}/tests/integration/test_examples.py +0 -0
  120. {fastled-1.4.41 → fastled-1.4.45}/tests/integration/test_libcompile.py +0 -0
  121. {fastled-1.4.41 → fastled-1.4.45}/tests/integration/test_playwright_integration.py +0 -0
  122. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/html/index.html +0 -0
  123. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_api.py +0 -0
  124. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_bad_ino.py +0 -0
  125. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_banner_string.py +0 -0
  126. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_cli.py +0 -0
  127. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_cli_no_platformio.py +0 -0
  128. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_compile_server.py +0 -0
  129. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_debug_fetch_source_files.py +0 -0
  130. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_docker_linux_on_windows.py +0 -0
  131. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_embedded_data.py +0 -0
  132. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_filechanger.py +0 -0
  133. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_flask_headers.py +0 -0
  134. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_header_dump.py +0 -0
  135. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_http_server.py +0 -0
  136. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_ino/bad/bad.ino +0 -0
  137. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_ino/bad_platformio/bad_platformio.ino +0 -0
  138. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_ino/bad_platformio/platformio.ini +0 -0
  139. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_ino/embedded/data/bigdata.dat +0 -0
  140. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_ino/embedded/wasm.ino +0 -0
  141. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_ino/wasm/wasm.ino +0 -0
  142. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_no_platformio_compile.py +0 -0
  143. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_project_init.py +0 -0
  144. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_server_and_client_seperatly.py +0 -0
  145. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_session_compile.py +0 -0
  146. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_string_diff.py +0 -0
  147. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_version.py +0 -0
  148. {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_webcompile.py +0 -0
  149. {fastled-1.4.41 → fastled-1.4.45}/upload_package.sh +0 -0
  150. {fastled-1.4.41 → fastled-1.4.45}/vscode-plugin/readme +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastled
3
- Version: 1.4.41
3
+ Version: 1.4.45
4
4
  Summary: FastLED Wasm Compiler
5
5
  Home-page: https://github.com/zackees/fastled-wasm
6
6
  Maintainer: Zachary Vorhies
@@ -1,6 +1,6 @@
1
1
  # IMPORTANT! There's a bug in github which will REJECT any version update
2
2
  # that has any other change in the repo. Please bump the version as the
3
3
  # ONLY change in a commit, or else the pypi update and the release will fail.
4
- __version__ = "1.4.41"
4
+ __version__ = "1.4.45"
5
5
 
6
6
  __version_url_latest__ = "https://raw.githubusercontent.com/zackees/fastled-wasm/refs/heads/main/src/fastled/__version__.py"
@@ -26,7 +26,6 @@ from docker.models.images import Image
26
26
  from filelock import FileLock
27
27
 
28
28
  from fastled.print_filter import PrintFilter, PrintFilterDefault
29
- from fastled.spinner import Spinner
30
29
 
31
30
  CONFIG_DIR = Path(user_data_dir("fastled", "fastled"))
32
31
  CONFIG_DIR.mkdir(parents=True, exist_ok=True)
@@ -252,7 +251,34 @@ class DockerManager:
252
251
  @property
253
252
  def client(self) -> DockerClient:
254
253
  if self._client is None:
255
- self._client = docker.from_env()
254
+ # Retry logic for WSL startup on Windows
255
+ max_retries = 10
256
+ retry_delay = 2 # seconds
257
+ last_error = None
258
+
259
+ for attempt in range(max_retries):
260
+ try:
261
+ self._client = docker.from_env()
262
+ if attempt > 0:
263
+ print(
264
+ f"Successfully connected to Docker after {attempt + 1} attempts"
265
+ )
266
+ return self._client
267
+ except DockerException as e:
268
+ last_error = e
269
+ if attempt < max_retries - 1:
270
+ if attempt == 0:
271
+ print("Waiting for Docker/WSL to be ready...")
272
+ print(
273
+ f"Attempt {attempt + 1}/{max_retries} failed, retrying in {retry_delay}s..."
274
+ )
275
+ time.sleep(retry_delay)
276
+ else:
277
+ print(
278
+ f"Failed to connect to Docker after {max_retries} attempts"
279
+ )
280
+ raise last_error
281
+ assert self._client is not None
256
282
  return self._client
257
283
 
258
284
  @staticmethod
@@ -507,14 +533,9 @@ class DockerManager:
507
533
  return False
508
534
 
509
535
  # Quick check for latest version
510
- with Spinner(f"Pulling newer version of {image_name}:{tag}..."):
511
- cmd_list = ["docker", "pull", f"{image_name}:{tag}"]
512
- subprocess.run(
513
- cmd_list,
514
- check=True,
515
- stdout=subprocess.DEVNULL,
516
- stderr=subprocess.DEVNULL,
517
- )
536
+ print(f"Pulling newer version of {image_name}:{tag}...")
537
+ cmd_list = ["docker", "pull", f"{image_name}:{tag}"]
538
+ subprocess.run(cmd_list, check=True)
518
539
  print(f"Updated to newer version of {image_name}:{tag}")
519
540
  local_image_hash = self.client.images.get(f"{image_name}:{tag}").id
520
541
  assert local_image_hash is not None
@@ -524,15 +545,10 @@ class DockerManager:
524
545
 
525
546
  except ImageNotFound:
526
547
  print(f"Image {image_name}:{tag} not found.")
527
- with Spinner("Loading "):
528
- # We use docker cli here because it shows the download.
529
- cmd_list = ["docker", "pull", f"{image_name}:{tag}"]
530
- subprocess.run(
531
- cmd_list,
532
- check=True,
533
- stdout=subprocess.DEVNULL,
534
- stderr=subprocess.DEVNULL,
535
- )
548
+ print("Loading...")
549
+ # We use docker cli here because it shows the download.
550
+ cmd_list = ["docker", "pull", f"{image_name}:{tag}"]
551
+ subprocess.run(cmd_list, check=True)
536
552
  try:
537
553
  local_image = self.client.images.get(f"{image_name}:{tag}")
538
554
  local_image_hash = local_image.id
@@ -22,20 +22,27 @@ def get_examples(host: str | None = None) -> list[str]:
22
22
 
23
23
 
24
24
  def _prompt_for_example() -> str:
25
+ from fastled.select_sketch_directory import _disambiguate_user_choice
26
+
25
27
  examples = get_examples()
26
- while True:
27
- print("Available examples:")
28
- for i, example in enumerate(examples):
29
- print(f" [{i+1}]: {example}")
30
- answer = input("Enter the example number or name: ").strip()
31
- if answer.isdigit():
32
- example_num = int(answer) - 1
33
- if example_num < 0 or example_num >= len(examples):
34
- print("Invalid example number")
35
- continue
36
- return examples[example_num]
37
- elif answer in examples:
38
- return answer
28
+
29
+ # Find default example index (prefer DEFAULT_EXAMPLE if it exists)
30
+ default_index = 0
31
+ if DEFAULT_EXAMPLE in examples:
32
+ default_index = examples.index(DEFAULT_EXAMPLE)
33
+
34
+ result = _disambiguate_user_choice(
35
+ examples,
36
+ option_to_str=lambda x: x,
37
+ prompt="Available examples:",
38
+ default_index=default_index,
39
+ )
40
+
41
+ if result is None:
42
+ # Fallback to DEFAULT_EXAMPLE if user cancelled
43
+ return DEFAULT_EXAMPLE
44
+
45
+ return result
39
46
 
40
47
 
41
48
  class DownloadThread(threading.Thread):
@@ -0,0 +1,122 @@
1
+ from pathlib import Path
2
+ from typing import Callable, TypeVar, Union
3
+
4
+ T = TypeVar("T")
5
+
6
+
7
+ def _disambiguate_user_choice(
8
+ options: list[T],
9
+ option_to_str: Callable[[T], str] = str,
10
+ prompt: str = "Multiple matches found. Please choose:",
11
+ default_index: int = 0,
12
+ ) -> Union[T, None]:
13
+ """
14
+ Present multiple options to the user with a default selection.
15
+
16
+ Args:
17
+ options: List of options to choose from
18
+ option_to_str: Function to convert option to display string
19
+ prompt: Prompt message to show user
20
+ default_index: Index of the default option (0-based)
21
+
22
+ Returns:
23
+ Selected option or None if cancelled
24
+ """
25
+ if not options:
26
+ return None
27
+
28
+ if len(options) == 1:
29
+ return options[0]
30
+
31
+ # Ensure default_index is valid
32
+ if default_index < 0 or default_index >= len(options):
33
+ default_index = 0
34
+
35
+ print(f"\n{prompt}")
36
+ for i, option in enumerate(options):
37
+ option_str = option_to_str(option)
38
+ if i == default_index:
39
+ print(f" [{i+1}]: [{option_str}]") # Default option shown in brackets
40
+ else:
41
+ print(f" [{i+1}]: {option_str}")
42
+
43
+ default_option_str = option_to_str(options[default_index])
44
+ user_input = input(
45
+ f"\nEnter number or name (default: [{default_option_str}]): "
46
+ ).strip()
47
+
48
+ # Handle empty input - select default
49
+ if not user_input:
50
+ return options[default_index]
51
+
52
+ # Try to parse as number
53
+ try:
54
+ index = int(user_input) - 1
55
+ if 0 <= index < len(options):
56
+ return options[index]
57
+ except ValueError:
58
+ pass
59
+
60
+ # Try to match by name (case insensitive)
61
+ user_input_lower = user_input.lower()
62
+ for option in options:
63
+ option_str = option_to_str(option).lower()
64
+ if option_str == user_input_lower:
65
+ return option
66
+
67
+ # Try partial match
68
+ matches = []
69
+ for option in options:
70
+ option_str = option_to_str(option)
71
+ if user_input_lower in option_str.lower():
72
+ matches.append(option)
73
+
74
+ if len(matches) == 1:
75
+ return matches[0]
76
+ elif len(matches) > 1:
77
+ # Recursive disambiguation with the filtered matches
78
+ return _disambiguate_user_choice(
79
+ matches,
80
+ option_to_str,
81
+ f"Multiple partial matches for '{user_input}':",
82
+ 0, # Reset default to first match
83
+ )
84
+
85
+ # No match found
86
+ print(f"No match found for '{user_input}'. Please try again.")
87
+ return _disambiguate_user_choice(options, option_to_str, prompt, default_index)
88
+
89
+
90
+ def select_sketch_directory(
91
+ sketch_directories: list[Path], cwd_is_fastled: bool, is_followup: bool = False
92
+ ) -> str | None:
93
+ if cwd_is_fastled:
94
+ exclude = ["src", "dev", "tests"]
95
+ for ex in exclude:
96
+ p = Path(ex)
97
+ if p in sketch_directories:
98
+ sketch_directories.remove(p)
99
+
100
+ if len(sketch_directories) == 1:
101
+ print(f"\nUsing sketch directory: {sketch_directories[0]}")
102
+ return str(sketch_directories[0])
103
+ elif len(sketch_directories) > 1:
104
+ # On first scan with >4 options, don't prompt - return None to signal ambiguity
105
+ if not is_followup and len(sketch_directories) > 4:
106
+ print(f"\nFound {len(sketch_directories)} sketch directories.")
107
+ print("Please specify a sketch directory to avoid ambiguity.")
108
+ return None
109
+
110
+ # Otherwise, prompt user to disambiguate
111
+ result = _disambiguate_user_choice(
112
+ sketch_directories,
113
+ option_to_str=lambda x: str(x),
114
+ prompt="Multiple Directories found, choose one:",
115
+ default_index=0,
116
+ )
117
+
118
+ if result is None:
119
+ return None
120
+
121
+ return str(result)
122
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastled
3
- Version: 1.4.41
3
+ Version: 1.4.45
4
4
  Summary: FastLED Wasm Compiler
5
5
  Home-page: https://github.com/zackees/fastled-wasm
6
6
  Maintainer: Zachary Vorhies
@@ -130,6 +130,7 @@ tests/unit/test_http_server.py
130
130
  tests/unit/test_manual_api_invocation.py
131
131
  tests/unit/test_no_platformio_compile.py
132
132
  tests/unit/test_project_init.py
133
+ tests/unit/test_select_sketch_directory.py
133
134
  tests/unit/test_server_and_client_seperatly.py
134
135
  tests/unit/test_session_compile.py
135
136
  tests/unit/test_string_diff.py
@@ -187,11 +187,14 @@ void loop() {
187
187
  self.assertGreater(len(wasm_files), 0, "Expected to find .wasm files")
188
188
 
189
189
  # Verify file sizes are reasonable
190
+ # Note: graphics_manager_base.js is intentionally empty/minimal
191
+ skip_size_check = {"graphics_manager_base.js"}
190
192
  for js_file in js_files:
191
193
  size = js_file.stat().st_size
192
- self.assertGreater(
193
- size, 1000, f"JS file {js_file.name} too small: {size} bytes"
194
- )
194
+ if js_file.name not in skip_size_check:
195
+ self.assertGreater(
196
+ size, 1000, f"JS file {js_file.name} too small: {size} bytes"
197
+ )
195
198
  print(f"📄 {js_file.name}: {size:,} bytes")
196
199
 
197
200
  for wasm_file in wasm_files:
@@ -0,0 +1,103 @@
1
+ """
2
+ Unit tests for select_sketch_directory to prevent regressions.
3
+ """
4
+
5
+ import unittest
6
+ from pathlib import Path
7
+ from unittest.mock import patch
8
+
9
+ from fastled.select_sketch_directory import select_sketch_directory
10
+
11
+
12
+ class TestSelectSketchDirectory(unittest.TestCase):
13
+ """Test select_sketch_directory behavior."""
14
+
15
+ def test_single_directory_auto_selects(self):
16
+ """Single directory should auto-select without prompting."""
17
+ sketch_dirs = [Path("sketch1")]
18
+ result = select_sketch_directory(sketch_dirs, cwd_is_fastled=False)
19
+ self.assertEqual(str(sketch_dirs[0]), result)
20
+
21
+ def test_first_scan_with_more_than_4_returns_none(self):
22
+ """First scan with >4 directories should return None (no auto-select)."""
23
+ sketch_dirs = [Path(f"sketch{i}") for i in range(5)]
24
+ result = select_sketch_directory(
25
+ sketch_dirs, cwd_is_fastled=False, is_followup=False
26
+ )
27
+ self.assertIsNone(result)
28
+
29
+ @patch("builtins.input", return_value="")
30
+ def test_first_scan_with_4_or_less_prompts_with_default(self, mock_input):
31
+ """First scan with ≤4 directories should prompt with default selection."""
32
+ sketch_dirs = [Path(f"sketch{i}") for i in range(4)]
33
+ result = select_sketch_directory(
34
+ sketch_dirs, cwd_is_fastled=False, is_followup=False
35
+ )
36
+ # User pressed return (empty input), should get default (first option)
37
+ self.assertEqual(str(sketch_dirs[0]), result)
38
+ mock_input.assert_called_once()
39
+
40
+ @patch("builtins.input", return_value="2")
41
+ def test_first_scan_user_selects_by_number(self, mock_input):
42
+ """User can select by number on first scan with ≤4 directories."""
43
+ sketch_dirs = [Path(f"sketch{i}") for i in range(4)]
44
+ result = select_sketch_directory(
45
+ sketch_dirs, cwd_is_fastled=False, is_followup=False
46
+ )
47
+ self.assertEqual(str(sketch_dirs[1]), result)
48
+ mock_input.assert_called_once()
49
+
50
+ @patch("builtins.input", return_value="")
51
+ def test_followup_always_prompts_even_with_many_dirs(self, mock_input):
52
+ """Follow-up calls should always prompt, even with >4 directories."""
53
+ sketch_dirs = [Path(f"sketch{i}") for i in range(10)]
54
+ result = select_sketch_directory(
55
+ sketch_dirs, cwd_is_fastled=False, is_followup=True
56
+ )
57
+ # Should prompt and return default (first option)
58
+ self.assertEqual(str(sketch_dirs[0]), result)
59
+ mock_input.assert_called_once()
60
+
61
+ @patch("builtins.input", return_value="5")
62
+ def test_followup_user_can_select_from_many(self, mock_input):
63
+ """Follow-up calls allow user to select from many directories."""
64
+ sketch_dirs = [Path(f"sketch{i}") for i in range(10)]
65
+ result = select_sketch_directory(
66
+ sketch_dirs, cwd_is_fastled=False, is_followup=True
67
+ )
68
+ self.assertEqual(str(sketch_dirs[4]), result)
69
+ mock_input.assert_called_once()
70
+
71
+ def test_fastled_repo_excludes_src_dev_tests(self):
72
+ """When in FastLED repo, should exclude src, dev, tests directories."""
73
+ sketch_dirs = [Path("src"), Path("dev"), Path("tests"), Path("examples")]
74
+ result = select_sketch_directory(
75
+ sketch_dirs, cwd_is_fastled=True, is_followup=False
76
+ )
77
+ # Only "examples" remains, should auto-select
78
+ self.assertEqual("examples", result)
79
+
80
+ @patch("builtins.input", return_value="sketch3")
81
+ def test_user_can_select_by_name(self, mock_input):
82
+ """User can select by name instead of number."""
83
+ sketch_dirs = [Path("sketch1"), Path("sketch2"), Path("sketch3")]
84
+ result = select_sketch_directory(
85
+ sketch_dirs, cwd_is_fastled=False, is_followup=False
86
+ )
87
+ self.assertEqual("sketch3", result)
88
+ mock_input.assert_called_once()
89
+
90
+ @patch("builtins.input", side_effect=["invalid", "1"])
91
+ def test_invalid_input_retry(self, mock_input):
92
+ """Invalid input should retry prompt."""
93
+ sketch_dirs = [Path("sketch1"), Path("sketch2")]
94
+ result = select_sketch_directory(
95
+ sketch_dirs, cwd_is_fastled=False, is_followup=False
96
+ )
97
+ self.assertEqual(str(sketch_dirs[0]), result)
98
+ # Should be called twice: once for invalid, once for valid
99
+ self.assertEqual(mock_input.call_count, 2)
100
+
101
+
102
+ if __name__ == "__main__":
103
+ unittest.main()
@@ -210,6 +210,41 @@ class TestStringDiffComprehensive:
210
210
  expected in result_names
211
211
  ), f"Expected '{expected}' in results for spaced input '{input_str}', got: {result_names}"
212
212
 
213
+ def test_whitespace_in_camelcase_names(self):
214
+ """Test that whitespace between words matches CamelCase names without disambiguation.
215
+
216
+ This tests the specific case where 'Noise Ring' should directly match 'FxNoiseRing'
217
+ without requiring a disambiguation step, since the whitespace is just separating
218
+ the CamelCase components.
219
+ """
220
+ test_cases = [
221
+ ("Noise Ring", "FxNoiseRing", 1), # Should be unique match
222
+ ("Noise Playground", "NoisePlayground", 1), # Should be unique match
223
+ ("Noise Plus Palette", "NoisePlusPalette", 1), # Should be unique match
224
+ ("Fire Cylinder", "FireCylinder", 1), # Should be unique match
225
+ ("Fire Matrix", "FireMatrix", 1), # Should be unique match
226
+ ("Blink Parallel", "BlinkParallel", 1), # Should be unique match
227
+ ("Color Palette", "ColorPalette", 1), # Should be unique match
228
+ ("Color Temperature", "ColorTemperature", 1), # Should be unique match
229
+ ("Demo Reel 100", "DemoReel100", 1), # Should be unique match
230
+ ("XY Matrix", "XYMatrix", 1), # Should be unique match
231
+ ("XY Path", "XYPath", 1), # Should be unique match
232
+ ]
233
+
234
+ for input_str, expected, expected_count in test_cases:
235
+ results = string_diff(input_str, self.fastled_examples)
236
+ result_names = [r[1] for r in results]
237
+
238
+ # Check that the expected match is in the results
239
+ assert (
240
+ expected in result_names
241
+ ), f"Expected '{expected}' in results for whitespace input '{input_str}', got: {result_names}"
242
+
243
+ # Check that it's a direct match without disambiguation (should return exactly expected_count results)
244
+ assert (
245
+ len(results) == expected_count
246
+ ), f"Expected exactly {expected_count} result(s) for '{input_str}' (no disambiguation), got {len(results)}: {result_names}"
247
+
213
248
  def test_typos_and_minor_edits(self):
214
249
  """Test handling of typos and minor edits."""
215
250
  test_cases = [
@@ -605,6 +640,7 @@ if __name__ == "__main__":
605
640
  test_instance.test_case_insensitive_matches,
606
641
  test_instance.test_partial_matches,
607
642
  test_instance.test_spaces_in_input,
643
+ test_instance.test_whitespace_in_camelcase_names,
608
644
  test_instance.test_typos_and_minor_edits,
609
645
  test_instance.test_prefix_matching,
610
646
  test_instance.test_suffix_matching,
@@ -1,67 +0,0 @@
1
- from pathlib import Path
2
-
3
- from rapidfuzz import fuzz
4
-
5
- from fastled.string_diff import string_diff_paths
6
-
7
-
8
- def select_sketch_directory(
9
- sketch_directories: list[Path], cwd_is_fastled: bool, is_followup: bool = False
10
- ) -> str | None:
11
- if cwd_is_fastled:
12
- exclude = ["src", "dev", "tests"]
13
- for ex in exclude:
14
- p = Path(ex)
15
- if p in sketch_directories:
16
- sketch_directories.remove(p)
17
-
18
- if len(sketch_directories) == 1:
19
- print(f"\nUsing sketch directory: {sketch_directories[0]}")
20
- return str(sketch_directories[0])
21
- elif len(sketch_directories) > 1:
22
- print("\nMultiple Directories found, choose one:")
23
- for i, sketch_dir in enumerate(sketch_directories):
24
- print(f" [{i+1}]: {sketch_dir}")
25
- which = input(
26
- "\nPlease specify a sketch directory\nYou can enter a number or type a fuzzy search: "
27
- ).strip()
28
- try:
29
- index = int(which) - 1
30
- return str(sketch_directories[index])
31
- except (ValueError, IndexError):
32
- inputs = [p for p in sketch_directories]
33
-
34
- if is_followup:
35
- # On follow-up, find the closest match by fuzzy distance
36
- distances = []
37
- for path in inputs:
38
- path_str = str(path).replace("\\", "/")
39
- dist = fuzz.token_sort_ratio(which.lower(), path_str.lower())
40
- distances.append((dist, path))
41
-
42
- # Get the best distance and return the closest match(es)
43
- best_distance = max(distances, key=lambda x: x[0])[0]
44
- best_matches = [
45
- path for dist, path in distances if dist == best_distance
46
- ]
47
-
48
- if len(best_matches) == 1:
49
- example = best_matches[0]
50
- return str(example)
51
- else:
52
- # If still multiple matches with same distance, recurse again
53
- return select_sketch_directory(
54
- best_matches, cwd_is_fastled, is_followup=True
55
- )
56
- else:
57
- # First call - use original fuzzy matching (allows ambiguity)
58
- top_hits: list[tuple[float, Path]] = string_diff_paths(which, inputs)
59
- if len(top_hits) == 1:
60
- example = top_hits[0][1]
61
- return str(example)
62
- else:
63
- # Recursive call with is_followup=True for more precise matching
64
- return select_sketch_directory(
65
- [p for _, p in top_hits], cwd_is_fastled, is_followup=True
66
- )
67
- return None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes