fastled 1.2.33__py3-none-any.whl → 1.4.50__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. fastled/__init__.py +51 -192
  2. fastled/__main__.py +14 -0
  3. fastled/__version__.py +6 -0
  4. fastled/app.py +124 -27
  5. fastled/args.py +124 -0
  6. fastled/assets/localhost-key.pem +28 -0
  7. fastled/assets/localhost.pem +27 -0
  8. fastled/cli.py +10 -2
  9. fastled/cli_test.py +21 -0
  10. fastled/cli_test_interactive.py +21 -0
  11. fastled/client_server.py +334 -55
  12. fastled/compile_server.py +12 -1
  13. fastled/compile_server_impl.py +115 -42
  14. fastled/docker_manager.py +392 -69
  15. fastled/emoji_util.py +27 -0
  16. fastled/filewatcher.py +100 -8
  17. fastled/find_good_connection.py +105 -0
  18. fastled/header_dump.py +63 -0
  19. fastled/install/__init__.py +1 -0
  20. fastled/install/examples_manager.py +62 -0
  21. fastled/install/extension_manager.py +113 -0
  22. fastled/install/main.py +156 -0
  23. fastled/install/project_detection.py +167 -0
  24. fastled/install/test_install.py +373 -0
  25. fastled/install/vscode_config.py +344 -0
  26. fastled/interruptible_http.py +148 -0
  27. fastled/keyboard.py +1 -0
  28. fastled/keyz.py +84 -0
  29. fastled/live_client.py +26 -1
  30. fastled/open_browser.py +133 -89
  31. fastled/parse_args.py +219 -15
  32. fastled/playwright/chrome_extension_downloader.py +207 -0
  33. fastled/playwright/playwright_browser.py +773 -0
  34. fastled/playwright/resize_tracking.py +127 -0
  35. fastled/print_filter.py +52 -0
  36. fastled/project_init.py +20 -13
  37. fastled/select_sketch_directory.py +142 -17
  38. fastled/server_flask.py +487 -0
  39. fastled/server_start.py +21 -0
  40. fastled/settings.py +53 -4
  41. fastled/site/build.py +2 -10
  42. fastled/site/examples.py +10 -0
  43. fastled/sketch.py +129 -7
  44. fastled/string_diff.py +218 -9
  45. fastled/test/examples.py +7 -5
  46. fastled/types.py +22 -2
  47. fastled/util.py +78 -0
  48. fastled/version.py +41 -0
  49. fastled/web_compile.py +401 -218
  50. fastled/zip_files.py +76 -0
  51. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/METADATA +533 -382
  52. fastled-1.4.50.dist-info/RECORD +60 -0
  53. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/WHEEL +1 -1
  54. fastled/open_browser2.py +0 -111
  55. fastled-1.2.33.dist-info/RECORD +0 -33
  56. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/entry_points.txt +0 -0
  57. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info/licenses}/LICENSE +0 -0
  58. {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,127 @@
1
+ """
2
+ Resize tracking for Playwright browser integration.
3
+
4
+ This module provides a class to track browser window resize events and adjust
5
+ the viewport accordingly without using internal polling loops.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+
11
+ class ResizeTracker:
12
+ """Tracks browser window resize events and adjusts viewport accordingly."""
13
+
14
+ def __init__(self, page: Any):
15
+ """Initialize the resize tracker.
16
+
17
+ Args:
18
+ page: The Playwright page object to track
19
+ """
20
+ self.page = page
21
+ self.last_outer_size: tuple[int, int] | None = None
22
+
23
+ async def _get_window_info(self) -> dict[str, int] | None:
24
+ """Get browser window dimensions information.
25
+
26
+ Returns:
27
+ Dictionary containing window dimensions or None if unable to retrieve
28
+ """
29
+ if self.page is None:
30
+ return None
31
+
32
+ try:
33
+ return await self.page.evaluate(
34
+ """
35
+ () => {
36
+ return {
37
+ outerWidth: window.outerWidth,
38
+ outerHeight: window.outerHeight,
39
+ innerWidth: window.innerWidth,
40
+ innerHeight: window.innerHeight,
41
+ contentWidth: document.documentElement.clientWidth,
42
+ contentHeight: document.documentElement.clientHeight
43
+ };
44
+ }
45
+ """
46
+ )
47
+ except Exception:
48
+ return None
49
+
50
+ async def set_viewport_size(self, width: int, height: int) -> None:
51
+ """Set the viewport size.
52
+
53
+ Args:
54
+ width: The viewport width
55
+ height: The viewport height
56
+ """
57
+ if self.page is None:
58
+ raise Exception("Page is None")
59
+
60
+ await self.page.set_viewport_size({"width": width, "height": height})
61
+
62
+ async def update(self) -> Exception | None:
63
+ """Update the resize tracking and adjust viewport if needed.
64
+
65
+ Returns:
66
+ None if successful, Exception if an error occurred
67
+ """
68
+ try:
69
+ # Check if page is still alive
70
+ if self.page is None or self.page.is_closed():
71
+ return Exception("Page closed")
72
+
73
+ window_info = await self._get_window_info()
74
+
75
+ if window_info:
76
+ current_outer = (
77
+ window_info["outerWidth"],
78
+ window_info["outerHeight"],
79
+ )
80
+
81
+ # Check if window size changed
82
+ if (
83
+ self.last_outer_size is None
84
+ or current_outer != self.last_outer_size
85
+ ):
86
+
87
+ if self.last_outer_size is not None:
88
+ print("[PYTHON] *** BROWSER WINDOW RESIZED ***")
89
+ print(
90
+ f"[PYTHON] Outer window changed from {self.last_outer_size[0]}x{self.last_outer_size[1]} to {current_outer[0]}x{current_outer[1]}"
91
+ )
92
+
93
+ self.last_outer_size = current_outer
94
+
95
+ # Set viewport to match the outer window size
96
+ outer_width = int(window_info["outerWidth"])
97
+ outer_height = int(window_info["outerHeight"])
98
+
99
+ print(
100
+ f"[PYTHON] Setting viewport to match outer window size: {outer_width}x{outer_height}"
101
+ )
102
+
103
+ await self.set_viewport_size(outer_width, outer_height)
104
+ print("[PYTHON] Viewport set successfully")
105
+
106
+ # Query the actual window dimensions after the viewport change
107
+ updated_window_info = await self._get_window_info()
108
+
109
+ if updated_window_info:
110
+ print(f"[PYTHON] Updated window info: {updated_window_info}")
111
+
112
+ # Update our tracking with the actual final outer size
113
+ self.last_outer_size = (
114
+ updated_window_info["outerWidth"],
115
+ updated_window_info["outerHeight"],
116
+ )
117
+ print(
118
+ f"[PYTHON] Updated last_outer_size to actual final size: {self.last_outer_size}"
119
+ )
120
+ else:
121
+ print("[PYTHON] Could not get updated window info")
122
+
123
+ except Exception as e:
124
+ return e
125
+
126
+ # Success case
127
+ return None
@@ -0,0 +1,52 @@
1
+ import re
2
+ from abc import ABC, abstractmethod
3
+ from enum import Enum
4
+
5
+
6
+ class PrintFilter(ABC):
7
+ """Abstract base class for filtering text output."""
8
+
9
+ def __init__(self, echo: bool = True) -> None:
10
+ self.echo = echo
11
+
12
+ @abstractmethod
13
+ def filter(self, text: str) -> str:
14
+ """Filter the text according to implementation-specific rules."""
15
+ pass
16
+
17
+ def print(self, text: str | bytes) -> str:
18
+ """Prints the text to the console after filtering."""
19
+ if isinstance(text, bytes):
20
+ text = text.decode("utf-8")
21
+ text = self.filter(text)
22
+ if self.echo:
23
+ print(text, end="")
24
+ return text
25
+
26
+
27
+ def _handle_ino_cpp(line: str) -> str:
28
+ if ".ino.cpp" in line[0:30]:
29
+ # Extract the filename without path and extension
30
+ match = re.search(r"src/([^/]+)\.ino\.cpp", line)
31
+ if match:
32
+ filename = match.group(1)
33
+ # Replace with examples/Filename/Filename.ino format
34
+ line = line.replace(
35
+ f"src/{filename}.ino.cpp", f"examples/{filename}/{filename}.ino"
36
+ )
37
+ else:
38
+ # Fall back to simple extension replacement if regex doesn't match
39
+ line = line.replace(".ino.cpp", ".ino")
40
+ return line
41
+
42
+
43
+ class PrintFilterDefault(PrintFilter):
44
+ """Provides default filtering for FastLED output."""
45
+
46
+ def filter(self, text: str) -> str:
47
+ return text
48
+
49
+
50
+ class CompileOrLink(Enum):
51
+ COMPILE = "compile"
52
+ LINK = "link"
fastled/project_init.py CHANGED
@@ -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):
@@ -1,10 +1,134 @@
1
1
  from pathlib import Path
2
+ from typing import Callable, TypeVar, Union
2
3
 
3
- from fastled.string_diff import string_diff_paths
4
+ from fastled.string_diff import string_diff
5
+
6
+ T = TypeVar("T")
7
+
8
+
9
+ def _disambiguate_user_choice(
10
+ options: list[T],
11
+ option_to_str: Callable[[T], str] = str,
12
+ prompt: str = "Multiple matches found. Please choose:",
13
+ default_index: int = 0,
14
+ ) -> Union[T, None]:
15
+ """
16
+ Present multiple options to the user with a default selection.
17
+
18
+ Args:
19
+ options: List of options to choose from
20
+ option_to_str: Function to convert option to display string
21
+ prompt: Prompt message to show user
22
+ default_index: Index of the default option (0-based)
23
+
24
+ Returns:
25
+ Selected option or None if cancelled
26
+ """
27
+ if not options:
28
+ return None
29
+
30
+ if len(options) == 1:
31
+ return options[0]
32
+
33
+ # Ensure default_index is valid
34
+ if default_index < 0 or default_index >= len(options):
35
+ default_index = 0
36
+
37
+ print(f"\n{prompt}")
38
+ for i, option in enumerate(options):
39
+ option_str = option_to_str(option)
40
+ if i == default_index:
41
+ print(f" [{i+1}]: [{option_str}]") # Default option shown in brackets
42
+ else:
43
+ print(f" [{i+1}]: {option_str}")
44
+
45
+ default_option_str = option_to_str(options[default_index])
46
+ user_input = input(
47
+ f"\nEnter number or name (default: [{default_option_str}]): "
48
+ ).strip()
49
+
50
+ # Handle empty input - select default
51
+ if not user_input:
52
+ return options[default_index]
53
+
54
+ # Try to parse as number
55
+ try:
56
+ index = int(user_input) - 1
57
+ if 0 <= index < len(options):
58
+ return options[index]
59
+ except ValueError:
60
+ pass
61
+
62
+ # Try to match by name (case insensitive)
63
+ user_input_lower = user_input.lower()
64
+ for option in options:
65
+ option_str = option_to_str(option).lower()
66
+ if option_str == user_input_lower:
67
+ return option
68
+
69
+ # Try partial match
70
+ matches = []
71
+ for option in options:
72
+ option_str = option_to_str(option)
73
+ if user_input_lower in option_str.lower():
74
+ matches.append(option)
75
+
76
+ if len(matches) == 1:
77
+ return matches[0]
78
+ elif len(matches) > 1:
79
+ # Recursive disambiguation with the filtered matches
80
+ return _disambiguate_user_choice(
81
+ matches,
82
+ option_to_str,
83
+ f"Multiple partial matches for '{user_input}':",
84
+ 0, # Reset default to first match
85
+ )
86
+
87
+ # Try fuzzy matching as fallback
88
+ # For better fuzzy matching on paths, extract just the last component (basename)
89
+ # to avoid the "examples/" prefix interfering with matching
90
+ from pathlib import Path as PathLib
91
+
92
+ option_basenames = []
93
+ for option in options:
94
+ option_str = option_to_str(option)
95
+ # Extract basename for fuzzy matching
96
+ basename = (
97
+ PathLib(option_str).name
98
+ if "/" in option_str or "\\" in option_str
99
+ else option_str
100
+ )
101
+ option_basenames.append(basename)
102
+
103
+ fuzzy_results = string_diff(user_input, option_basenames)
104
+
105
+ if fuzzy_results:
106
+ # Map fuzzy results back to original options
107
+ fuzzy_matches = []
108
+ for _, matched_basename in fuzzy_results:
109
+ for i, basename in enumerate(option_basenames):
110
+ if basename == matched_basename:
111
+ fuzzy_matches.append(options[i])
112
+ break
113
+
114
+ if len(fuzzy_matches) == 1:
115
+ return fuzzy_matches[0]
116
+ elif len(fuzzy_matches) > 1:
117
+ # Recursive disambiguation with fuzzy matches
118
+ return _disambiguate_user_choice(
119
+ fuzzy_matches,
120
+ option_to_str,
121
+ f"Multiple fuzzy matches for '{user_input}':",
122
+ 0,
123
+ )
124
+
125
+ # No match found
126
+ print(f"No match found for '{user_input}'. Please try again.")
127
+ return _disambiguate_user_choice(options, option_to_str, prompt, default_index)
4
128
 
5
129
 
6
130
  def select_sketch_directory(
7
- sketch_directories: list[Path], cwd_is_fastled: bool
131
+ sketch_directories: list[Path], cwd_is_fastled: bool, is_followup: bool = False
8
132
  ) -> str | None:
9
133
  if cwd_is_fastled:
10
134
  exclude = ["src", "dev", "tests"]
@@ -17,19 +141,20 @@ def select_sketch_directory(
17
141
  print(f"\nUsing sketch directory: {sketch_directories[0]}")
18
142
  return str(sketch_directories[0])
19
143
  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: ").strip()
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[float, 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)
144
+ # First scan with >4 directories: return None (too many to auto-select)
145
+ if not is_followup and len(sketch_directories) > 4:
146
+ return None
147
+
148
+ # Prompt user to disambiguate
149
+ result = _disambiguate_user_choice(
150
+ sketch_directories,
151
+ option_to_str=lambda x: str(x),
152
+ prompt="Multiple Directories found, choose one:",
153
+ default_index=0,
154
+ )
155
+
156
+ if result is None:
157
+ return None
158
+
159
+ return str(result)
35
160
  return None