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.
- {fastled-1.4.41 → fastled-1.4.45}/PKG-INFO +1 -1
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/__version__.py +1 -1
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/docker_manager.py +35 -19
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/project_init.py +20 -13
- fastled-1.4.45/src/fastled/select_sketch_directory.py +122 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled.egg-info/PKG-INFO +1 -1
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled.egg-info/SOURCES.txt +1 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_manual_api_invocation.py +6 -3
- fastled-1.4.45/tests/unit/test_select_sketch_directory.py +103 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_string_diff_comprehensive.py +36 -0
- fastled-1.4.41/src/fastled/select_sketch_directory.py +0 -67
- {fastled-1.4.41 → fastled-1.4.45}/.aiderignore +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/.claude/settings.local.json +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/.cursorrules +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/.dockerignore +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/.github/workflows/build_multi_docker_image.yml +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/.github/workflows/build_webpage.yml +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/.github/workflows/lint.yml +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/.github/workflows/publish_release.yml +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/.github/workflows/template_build_docker_image.yml +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/.github/workflows/test_build_exe.yml +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/.github/workflows/test_macos.yml +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/.github/workflows/test_ubuntu.yml +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/.github/workflows/test_win.yml +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/.gitignore +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/.pylintrc +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/.vscode/launch.json +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/.vscode/settings.json +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/.vscode/tasks.json +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/DEBUGGER.md +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/Dockerfile +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/EXTENDED_INO.md +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/FAQ.md +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/LICENSE +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/MANIFEST.in +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/README.md +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/RELEASE.md +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/TODO.md +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/build_exe.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/build_local_docker.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/build_site.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/chrome_vscode_bridge_design_task.md +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/clean +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/compiler/debug.sh +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/compiler/run.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/demo/100dots.html +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/demo/demo_threejs.html +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/demo/micdemo.html +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/demo/mp3upload.html +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/demo/webgl_postprocessing_unreal_bloom.html +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/docker-compose.yml +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/entrypoint.sh +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/install +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/install_linux.sh +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/lint +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/pyproject.toml +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/requirements.docker.txt +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/requirements.testing.txt +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/setup.cfg +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/setup.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/__init__.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/__main__.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/app.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/args.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/assets/example.txt +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/assets/localhost-key.pem +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/assets/localhost.pem +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/cli.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/cli_test.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/cli_test_interactive.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/client_server.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/compile_server.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/compile_server_impl.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/emoji_util.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/filewatcher.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/find_good_connection.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/header_dump.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/install/__init__.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/install/examples_manager.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/install/extension_manager.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/install/main.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/install/project_detection.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/install/test_install.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/install/vscode_config.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/interruptible_http.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/keyboard.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/keyz.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/live_client.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/open_browser.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/parse_args.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/paths.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/playwright/chrome_extension_downloader.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/playwright/playwright_browser.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/playwright/resize_tracking.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/print_filter.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/server_flask.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/server_start.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/settings.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/site/build.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/site/examples.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/sketch.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/spinner.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/string_diff.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/test/can_run_local_docker_tests.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/test/examples.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/types.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/util.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/version.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/web_compile.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled/zip_files.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled.egg-info/dependency_links.txt +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled.egg-info/entry_points.txt +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled.egg-info/requires.txt +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/src/fastled.egg-info/top_level.txt +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/task.md +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/task2.md +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/test +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/integration/test_build_examples.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/integration/test_examples.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/integration/test_libcompile.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/integration/test_playwright_integration.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/html/index.html +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_api.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_bad_ino.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_banner_string.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_cli.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_cli_no_platformio.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_compile_server.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_debug_fetch_source_files.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_docker_linux_on_windows.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_embedded_data.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_filechanger.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_flask_headers.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_header_dump.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_http_server.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_ino/bad/bad.ino +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_ino/bad_platformio/bad_platformio.ino +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_ino/bad_platformio/platformio.ini +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_ino/embedded/data/bigdata.dat +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_ino/embedded/wasm.ino +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_ino/wasm/wasm.ino +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_no_platformio_compile.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_project_init.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_server_and_client_seperatly.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_session_compile.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_string_diff.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_version.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/tests/unit/test_webcompile.py +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/upload_package.sh +0 -0
- {fastled-1.4.41 → fastled-1.4.45}/vscode-plugin/readme +0 -0
@@ -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.
|
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
|
-
|
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
|
-
|
511
|
-
|
512
|
-
|
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
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
@@ -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
|
-
|
193
|
-
|
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
|
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
|
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
|