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.
- fastled/__init__.py +51 -192
- fastled/__main__.py +14 -0
- fastled/__version__.py +6 -0
- fastled/app.py +124 -27
- fastled/args.py +124 -0
- fastled/assets/localhost-key.pem +28 -0
- fastled/assets/localhost.pem +27 -0
- fastled/cli.py +10 -2
- fastled/cli_test.py +21 -0
- fastled/cli_test_interactive.py +21 -0
- fastled/client_server.py +334 -55
- fastled/compile_server.py +12 -1
- fastled/compile_server_impl.py +115 -42
- fastled/docker_manager.py +392 -69
- fastled/emoji_util.py +27 -0
- fastled/filewatcher.py +100 -8
- fastled/find_good_connection.py +105 -0
- fastled/header_dump.py +63 -0
- fastled/install/__init__.py +1 -0
- fastled/install/examples_manager.py +62 -0
- fastled/install/extension_manager.py +113 -0
- fastled/install/main.py +156 -0
- fastled/install/project_detection.py +167 -0
- fastled/install/test_install.py +373 -0
- fastled/install/vscode_config.py +344 -0
- fastled/interruptible_http.py +148 -0
- fastled/keyboard.py +1 -0
- fastled/keyz.py +84 -0
- fastled/live_client.py +26 -1
- fastled/open_browser.py +133 -89
- fastled/parse_args.py +219 -15
- fastled/playwright/chrome_extension_downloader.py +207 -0
- fastled/playwright/playwright_browser.py +773 -0
- fastled/playwright/resize_tracking.py +127 -0
- fastled/print_filter.py +52 -0
- fastled/project_init.py +20 -13
- fastled/select_sketch_directory.py +142 -17
- fastled/server_flask.py +487 -0
- fastled/server_start.py +21 -0
- fastled/settings.py +53 -4
- fastled/site/build.py +2 -10
- fastled/site/examples.py +10 -0
- fastled/sketch.py +129 -7
- fastled/string_diff.py +218 -9
- fastled/test/examples.py +7 -5
- fastled/types.py +22 -2
- fastled/util.py +78 -0
- fastled/version.py +41 -0
- fastled/web_compile.py +401 -218
- fastled/zip_files.py +76 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/METADATA +533 -382
- fastled-1.4.50.dist-info/RECORD +60 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/WHEEL +1 -1
- fastled/open_browser2.py +0 -111
- fastled-1.2.33.dist-info/RECORD +0 -33
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info}/entry_points.txt +0 -0
- {fastled-1.2.33.dist-info → fastled-1.4.50.dist-info/licenses}/LICENSE +0 -0
- {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
|
fastled/print_filter.py
ADDED
|
@@ -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
|
-
|
|
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):
|
|
@@ -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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|