fastled 1.4.18__py3-none-any.whl → 1.4.20__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/__version__.py CHANGED
@@ -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.18"
4
+ __version__ = "1.4.20"
5
5
 
6
6
  __version_url_latest__ = "https://raw.githubusercontent.com/zackees/fastled-wasm/refs/heads/main/src/fastled/__version__.py"
fastled/args.py CHANGED
@@ -16,6 +16,7 @@ class Args:
16
16
  app: bool # New flag to trigger Playwright browser with browser download if needed
17
17
  auto_update: bool | None
18
18
  update: bool
19
+ background_update: bool
19
20
  localhost: bool
20
21
  build: bool
21
22
  server: bool
@@ -57,6 +58,9 @@ class Args:
57
58
  args.no_auto_updates, bool | None
58
59
  ), f"expected bool | None, got {type(args.no_auto_updates)}"
59
60
  assert isinstance(args.update, bool), f"expected bool, got {type(args.update)}"
61
+ assert isinstance(
62
+ args.background_update, bool
63
+ ), f"expected bool, got {type(args.background_update)}"
60
64
  assert isinstance(
61
65
  args.localhost, bool
62
66
  ), f"expected bool, got {type(args.localhost)}"
@@ -88,6 +92,7 @@ class Args:
88
92
  app=args.app,
89
93
  auto_update=not args.no_auto_updates,
90
94
  update=args.update,
95
+ background_update=args.background_update,
91
96
  localhost=args.localhost,
92
97
  build=args.build,
93
98
  server=args.server,
fastled/client_server.py CHANGED
@@ -243,14 +243,35 @@ def _try_make_compile_server(
243
243
  return None
244
244
 
245
245
 
246
- def _is_local_host(host: str) -> bool:
246
+ def _background_update_docker_image() -> None:
247
+ """Perform docker image update in the background."""
248
+ try:
249
+ print("\n🔄 Starting background update of docker image...")
250
+ docker_manager = DockerManager()
251
+ updated = docker_manager.validate_or_download_image(
252
+ image_name=IMAGE_NAME, tag="latest", upgrade=True
253
+ )
254
+ if updated:
255
+ print("✅ Background docker image update completed successfully.")
256
+ else:
257
+ print("ℹ️ Docker image was already up to date.")
258
+ except KeyboardInterrupt:
259
+ print("⚠️ Background docker image update interrupted by user.")
260
+ import _thread
261
+
262
+ _thread.interrupt_main()
263
+ except Exception as e:
264
+ print(f"⚠️ Background docker image update failed: {e}")
265
+
266
+
267
+ def _is_local_host(url: str) -> bool:
247
268
  return (
248
- host.startswith("http://localhost")
249
- or host.startswith("http://127.0.0.1")
250
- or host.startswith("http://0.0.0.0")
251
- or host.startswith("http://[::]")
252
- or host.startswith("http://[::1]")
253
- or host.startswith("http://[::ffff:127.0.0.1]")
269
+ url.startswith("http://localhost")
270
+ or url.startswith("http://127.0.0.1")
271
+ or url.startswith("http://0.0.0.0")
272
+ or url.startswith("http://[::]")
273
+ or url.startswith("http://[::1]")
274
+ or url.startswith("http://[::ffff:127.0.0.1]")
254
275
  )
255
276
 
256
277
 
@@ -268,6 +289,7 @@ def run_client(
268
289
  clear: bool = False,
269
290
  no_platformio: bool = False,
270
291
  app: bool = False, # Use app-like browser experience
292
+ background_update: bool = False,
271
293
  ) -> int:
272
294
  has_checked_newer_version_yet = False
273
295
  compile_server: CompileServer | None = None
@@ -426,9 +448,20 @@ def run_client(
426
448
  )
427
449
  if has_update:
428
450
  print(f"\n🔄 {message}")
429
- print(
430
- "Run with `fastled -u` to update the docker image to the latest version."
431
- )
451
+ if background_update:
452
+ # Start background update in a separate thread
453
+ update_thread = threading.Thread(
454
+ target=_background_update_docker_image, daemon=True
455
+ )
456
+ update_thread.start()
457
+ background_update = False
458
+ else:
459
+ print(
460
+ "Run with `fastled -u` to update the docker image to the latest version."
461
+ )
462
+ print(
463
+ "Or use `--background-update` to update automatically in the background after compilation."
464
+ )
432
465
  except Exception as e:
433
466
  # Don't let Docker check failures interrupt the main flow
434
467
  warnings.warn(f"Failed to check for Docker image updates: {e}")
@@ -513,6 +546,7 @@ def run_client_server(args: Args) -> int:
513
546
  profile = bool(args.profile)
514
547
  web: str | bool = args.web if isinstance(args.web, str) else bool(args.web)
515
548
  auto_update = bool(args.auto_update)
549
+ background_update = bool(args.background_update)
516
550
  localhost = bool(args.localhost)
517
551
  directory = args.directory if args.directory else Path(".")
518
552
  just_compile = bool(args.just_compile)
@@ -585,6 +619,7 @@ def run_client_server(args: Args) -> int:
585
619
  clear=args.clear,
586
620
  no_platformio=no_platformio,
587
621
  app=app,
622
+ background_update=background_update,
588
623
  )
589
624
  except KeyboardInterrupt:
590
625
  return 1
fastled/filewatcher.py CHANGED
@@ -122,6 +122,9 @@ class FileChangedNotifier(threading.Thread):
122
122
  time.sleep(0.1)
123
123
  except KeyboardInterrupt:
124
124
  print("File watcher stopped by user.")
125
+ import _thread
126
+
127
+ _thread.interrupt_main()
125
128
  finally:
126
129
  self.stop()
127
130
 
fastled/parse_args.py CHANGED
@@ -34,6 +34,7 @@ FastLED WASM Compiler - Useful options:
34
34
  --quick Build in quick mode (default)
35
35
  --profile Enable profiling the C++ build system
36
36
  --update Update the docker image for the wasm compiler
37
+ --background-update Update the docker image in the background after compilation
37
38
  --purge Remove all FastLED containers and images
38
39
  --version Show version information
39
40
  --help Show detailed help
@@ -41,6 +42,7 @@ Examples:
41
42
  fastled (will auto detect the sketch directory and prompt you)
42
43
  fastled my_sketch
43
44
  fastled my_sketch --web (compiles using the web compiler only)
45
+ fastled my_sketch --background-update (compiles and updates docker image in background)
44
46
  fastled --init Blink (initializes a new sketch directory with the Blink example)
45
47
  fastled --server (runs the compiler server in the current directory)
46
48
 
@@ -132,6 +134,11 @@ def parse_args() -> Args:
132
134
  action="store_true",
133
135
  help="Update the wasm compiler (if necessary) before running",
134
136
  )
137
+ parser.add_argument(
138
+ "--background-update",
139
+ action="store_true",
140
+ help="Update the docker image in the background after compilation (user doesn't have to wait)",
141
+ )
135
142
  parser.add_argument(
136
143
  "--localhost",
137
144
  "--local",
@@ -562,6 +562,11 @@ class PlaywrightBrowserProxy:
562
562
  # Force exit the entire program
563
563
  os._exit(0)
564
564
 
565
+ except KeyboardInterrupt:
566
+ print("[MAIN] Browser monitor interrupted by user")
567
+ import _thread
568
+
569
+ _thread.interrupt_main()
565
570
  except Exception as e:
566
571
  print(f"[MAIN] Error monitoring browser process: {e}")
567
572
 
fastled/print_filter.py CHANGED
@@ -1,52 +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"
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/string_diff.py CHANGED
@@ -1,165 +1,165 @@
1
- from pathlib import Path
2
-
3
- from rapidfuzz import fuzz
4
-
5
-
6
- def _filter_out_obvious_bad_choices(
7
- input_str: str, string_list: list[str]
8
- ) -> list[str]:
9
- """
10
- Filter out strings that are too different from the input string.
11
- This is a heuristic and may not be perfect.
12
- """
13
- if not input_str.strip(): # Handle empty input
14
- return string_list
15
-
16
- input_chars = set(input_str.lower())
17
- filtered_list = []
18
- for s in string_list:
19
- # Check if at least half of the input characters are in the string
20
- s_chars = set(s.lower())
21
- common_chars = input_chars.intersection(s_chars)
22
- if len(common_chars) >= len(input_chars) / 2:
23
- filtered_list.append(s)
24
- return filtered_list
25
-
26
-
27
- def is_in_order_match(input_str: str, other: str) -> bool:
28
- """
29
- Check if the input string is an in-order match for any string in the list.
30
- An in-order match means that the characters of the input string appear
31
- in the same order in the string from the list, ignoring spaces in the input.
32
- """
33
-
34
- # Remove spaces from input string for matching
35
- input_chars = [c.lower() for c in input_str if c != " "]
36
- other_chars = [c.lower() for c in other]
37
- input_index = 0
38
- other_index = 0
39
- while input_index < len(input_chars) and other_index < len(other_chars):
40
- if input_chars[input_index] == other_chars[other_index]:
41
- input_index += 1
42
- other_index += 1
43
- # If we reached the end of the input string, it means all characters were found in order
44
- if input_index == len(input_chars):
45
- return True
46
- return False
47
-
48
-
49
- # Returns the min distance strings. If there is a tie, it returns
50
- # all the strings that have the same min distance.
51
- # Returns a tuple of index and string.
52
- def string_diff(
53
- input_string: str, string_list: list[str], ignore_case=True
54
- ) -> list[tuple[float, str]]:
55
-
56
- def normalize(s: str) -> str:
57
- return s.lower() if ignore_case else s
58
-
59
- # Handle empty input or empty list
60
- if not input_string.strip():
61
- # Return all strings with equal distance for empty input
62
- return [(i, s) for i, s in enumerate(string_list)]
63
-
64
- if not string_list:
65
- return []
66
-
67
- map_string: dict[str, str] = {}
68
-
69
- if ignore_case:
70
- map_string = {s.lower(): s for s in string_list}
71
- else:
72
- map_string = {s: s for s in string_list}
73
-
74
- original_string_list = string_list.copy()
75
- if ignore_case:
76
- string_list = [s.lower() for s in string_list]
77
- input_string = input_string.lower()
78
-
79
- # Check for exact matches, but also check if there are other substring matches
80
- exact_matches = [s for s in string_list if s == input_string]
81
- substring_matches = [s for s in string_list if input_string in s]
82
-
83
- # If there's an exact match AND other substring matches, return all substring matches
84
- # This provides better user experience for partial matching
85
- if exact_matches and len(substring_matches) > 1:
86
- out: list[tuple[float, str]] = []
87
- for i, s in enumerate(substring_matches):
88
- s_mapped = map_string.get(s, s)
89
- out.append((i, s_mapped))
90
- return out
91
-
92
- # If there's only an exact match and no other substring matches, return just the exact match
93
- if exact_matches and len(substring_matches) == 1:
94
- out: list[tuple[float, str]] = []
95
- for i, s in enumerate(exact_matches):
96
- s_mapped = map_string.get(s, s)
97
- out.append((i, s_mapped))
98
- return out
99
-
100
- # Apply set membership filtering for queries with 3+ characters
101
- if len(input_string.strip()) >= 3:
102
- filtered = _filter_out_obvious_bad_choices(input_string, string_list)
103
- if filtered: # Only apply filter if it doesn't eliminate everything
104
- string_list = filtered
105
-
106
- # Second filter: exact substring filtering if applicable
107
- if substring_matches:
108
- string_list = substring_matches
109
- # Return all substring matches
110
- out: list[tuple[float, str]] = []
111
- for i, s in enumerate(string_list):
112
- s_mapped = map_string.get(s, s)
113
- out.append((i, s_mapped))
114
- return out
115
-
116
- # Third filter: in order exact match filtering if applicable.
117
- in_order_matches = [s for s in string_list if is_in_order_match(input_string, s)]
118
- if in_order_matches:
119
- string_list = in_order_matches
120
-
121
- # Calculate distances
122
- distances: list[float] = []
123
- for s in string_list:
124
- dist = fuzz.token_sort_ratio(normalize(input_string), normalize(s))
125
- distances.append(1.0 / (dist + 1.0))
126
-
127
- # Handle case where no strings remain after filtering
128
- if not distances:
129
- # Fall back to original list and calculate distances
130
- string_list = original_string_list.copy()
131
- if ignore_case:
132
- string_list = [s.lower() for s in string_list]
133
-
134
- distances = []
135
- for s in string_list:
136
- dist = fuzz.token_sort_ratio(normalize(input_string), normalize(s))
137
- distances.append(1.0 / (dist + 1.0))
138
-
139
- min_distance = min(distances)
140
- out: list[tuple[float, str]] = []
141
- for i, d in enumerate(distances):
142
- if d == min_distance:
143
- s = string_list[i]
144
- s_mapped = map_string.get(s, s)
145
- out.append((i, s_mapped))
146
-
147
- return out
148
-
149
-
150
- def string_diff_paths(
151
- input_string: str | Path, path_list: list[Path], ignore_case=True
152
- ) -> list[tuple[float, Path]]:
153
- # Normalize path separators to forward slashes for consistent comparison
154
- string_list = [str(p).replace("\\", "/") for p in path_list]
155
- input_str = str(input_string).replace("\\", "/")
156
-
157
- tmp = string_diff(input_str, string_list, ignore_case)
158
- out: list[tuple[float, Path]] = []
159
- for i, j in tmp:
160
- # Find the original path that matches the normalized result
161
- for idx, orig_path in enumerate(path_list):
162
- if str(orig_path).replace("\\", "/") == j:
163
- out.append((i, orig_path))
164
- break
165
- return out
1
+ from pathlib import Path
2
+
3
+ from rapidfuzz import fuzz
4
+
5
+
6
+ def _filter_out_obvious_bad_choices(
7
+ input_str: str, string_list: list[str]
8
+ ) -> list[str]:
9
+ """
10
+ Filter out strings that are too different from the input string.
11
+ This is a heuristic and may not be perfect.
12
+ """
13
+ if not input_str.strip(): # Handle empty input
14
+ return string_list
15
+
16
+ input_chars = set(input_str.lower())
17
+ filtered_list = []
18
+ for s in string_list:
19
+ # Check if at least half of the input characters are in the string
20
+ s_chars = set(s.lower())
21
+ common_chars = input_chars.intersection(s_chars)
22
+ if len(common_chars) >= len(input_chars) / 2:
23
+ filtered_list.append(s)
24
+ return filtered_list
25
+
26
+
27
+ def is_in_order_match(input_str: str, other: str) -> bool:
28
+ """
29
+ Check if the input string is an in-order match for any string in the list.
30
+ An in-order match means that the characters of the input string appear
31
+ in the same order in the string from the list, ignoring spaces in the input.
32
+ """
33
+
34
+ # Remove spaces from input string for matching
35
+ input_chars = [c.lower() for c in input_str if c != " "]
36
+ other_chars = [c.lower() for c in other]
37
+ input_index = 0
38
+ other_index = 0
39
+ while input_index < len(input_chars) and other_index < len(other_chars):
40
+ if input_chars[input_index] == other_chars[other_index]:
41
+ input_index += 1
42
+ other_index += 1
43
+ # If we reached the end of the input string, it means all characters were found in order
44
+ if input_index == len(input_chars):
45
+ return True
46
+ return False
47
+
48
+
49
+ # Returns the min distance strings. If there is a tie, it returns
50
+ # all the strings that have the same min distance.
51
+ # Returns a tuple of index and string.
52
+ def string_diff(
53
+ input_string: str, string_list: list[str], ignore_case=True
54
+ ) -> list[tuple[float, str]]:
55
+
56
+ def normalize(s: str) -> str:
57
+ return s.lower() if ignore_case else s
58
+
59
+ # Handle empty input or empty list
60
+ if not input_string.strip():
61
+ # Return all strings with equal distance for empty input
62
+ return [(i, s) for i, s in enumerate(string_list)]
63
+
64
+ if not string_list:
65
+ return []
66
+
67
+ map_string: dict[str, str] = {}
68
+
69
+ if ignore_case:
70
+ map_string = {s.lower(): s for s in string_list}
71
+ else:
72
+ map_string = {s: s for s in string_list}
73
+
74
+ original_string_list = string_list.copy()
75
+ if ignore_case:
76
+ string_list = [s.lower() for s in string_list]
77
+ input_string = input_string.lower()
78
+
79
+ # Check for exact matches, but also check if there are other substring matches
80
+ exact_matches = [s for s in string_list if s == input_string]
81
+ substring_matches = [s for s in string_list if input_string in s]
82
+
83
+ # If there's an exact match AND other substring matches, return all substring matches
84
+ # This provides better user experience for partial matching
85
+ if exact_matches and len(substring_matches) > 1:
86
+ out: list[tuple[float, str]] = []
87
+ for i, s in enumerate(substring_matches):
88
+ s_mapped = map_string.get(s, s)
89
+ out.append((i, s_mapped))
90
+ return out
91
+
92
+ # If there's only an exact match and no other substring matches, return just the exact match
93
+ if exact_matches and len(substring_matches) == 1:
94
+ out: list[tuple[float, str]] = []
95
+ for i, s in enumerate(exact_matches):
96
+ s_mapped = map_string.get(s, s)
97
+ out.append((i, s_mapped))
98
+ return out
99
+
100
+ # Apply set membership filtering for queries with 3+ characters
101
+ if len(input_string.strip()) >= 3:
102
+ filtered = _filter_out_obvious_bad_choices(input_string, string_list)
103
+ if filtered: # Only apply filter if it doesn't eliminate everything
104
+ string_list = filtered
105
+
106
+ # Second filter: exact substring filtering if applicable
107
+ if substring_matches:
108
+ string_list = substring_matches
109
+ # Return all substring matches
110
+ out: list[tuple[float, str]] = []
111
+ for i, s in enumerate(string_list):
112
+ s_mapped = map_string.get(s, s)
113
+ out.append((i, s_mapped))
114
+ return out
115
+
116
+ # Third filter: in order exact match filtering if applicable.
117
+ in_order_matches = [s for s in string_list if is_in_order_match(input_string, s)]
118
+ if in_order_matches:
119
+ string_list = in_order_matches
120
+
121
+ # Calculate distances
122
+ distances: list[float] = []
123
+ for s in string_list:
124
+ dist = fuzz.token_sort_ratio(normalize(input_string), normalize(s))
125
+ distances.append(1.0 / (dist + 1.0))
126
+
127
+ # Handle case where no strings remain after filtering
128
+ if not distances:
129
+ # Fall back to original list and calculate distances
130
+ string_list = original_string_list.copy()
131
+ if ignore_case:
132
+ string_list = [s.lower() for s in string_list]
133
+
134
+ distances = []
135
+ for s in string_list:
136
+ dist = fuzz.token_sort_ratio(normalize(input_string), normalize(s))
137
+ distances.append(1.0 / (dist + 1.0))
138
+
139
+ min_distance = min(distances)
140
+ out: list[tuple[float, str]] = []
141
+ for i, d in enumerate(distances):
142
+ if d == min_distance:
143
+ s = string_list[i]
144
+ s_mapped = map_string.get(s, s)
145
+ out.append((i, s_mapped))
146
+
147
+ return out
148
+
149
+
150
+ def string_diff_paths(
151
+ input_string: str | Path, path_list: list[Path], ignore_case=True
152
+ ) -> list[tuple[float, Path]]:
153
+ # Normalize path separators to forward slashes for consistent comparison
154
+ string_list = [str(p).replace("\\", "/") for p in path_list]
155
+ input_str = str(input_string).replace("\\", "/")
156
+
157
+ tmp = string_diff(input_str, string_list, ignore_case)
158
+ out: list[tuple[float, Path]] = []
159
+ for i, j in tmp:
160
+ # Find the original path that matches the normalized result
161
+ for idx, orig_path in enumerate(path_list):
162
+ if str(orig_path).replace("\\", "/") == j:
163
+ out.append((i, orig_path))
164
+ break
165
+ return out