fastled 1.4.30__py3-none-any.whl → 1.4.32__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 +1 -1
- fastled/app.py +10 -0
- fastled/args.py +15 -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 +167 -0
- fastled/open_browser.py +7 -4
- fastled/parse_args.py +45 -0
- fastled/playwright/playwright_browser.py +75 -120
- fastled/playwright/resize_tracking.py +127 -0
- fastled/print_filter.py +52 -52
- fastled/string_diff.py +165 -165
- fastled/version.py +41 -41
- {fastled-1.4.30.dist-info → fastled-1.4.32.dist-info}/METADATA +531 -531
- {fastled-1.4.30.dist-info → fastled-1.4.32.dist-info}/RECORD +23 -15
- {fastled-1.4.30.dist-info → fastled-1.4.32.dist-info}/WHEEL +0 -0
- {fastled-1.4.30.dist-info → fastled-1.4.32.dist-info}/entry_points.txt +0 -0
- {fastled-1.4.30.dist-info → fastled-1.4.32.dist-info}/licenses/LICENSE +0 -0
- {fastled-1.4.30.dist-info → fastled-1.4.32.dist-info}/top_level.txt +0 -0
@@ -14,6 +14,8 @@ import warnings
|
|
14
14
|
from pathlib import Path
|
15
15
|
from typing import Any
|
16
16
|
|
17
|
+
from fastled.playwright.resize_tracking import ResizeTracker
|
18
|
+
|
17
19
|
# Set custom Playwright browser installation path
|
18
20
|
PLAYWRIGHT_DIR = Path.home() / ".fastled" / "playwright"
|
19
21
|
PLAYWRIGHT_DIR.mkdir(parents=True, exist_ok=True)
|
@@ -69,6 +71,7 @@ class PlaywrightBrowser:
|
|
69
71
|
self.playwright: Any = None
|
70
72
|
self._should_exit = asyncio.Event()
|
71
73
|
self._extensions_dir: Path | None = None
|
74
|
+
self.resize_tracker: ResizeTracker | None = None
|
72
75
|
|
73
76
|
# Initialize extensions if enabled
|
74
77
|
if self.enable_extensions:
|
@@ -278,148 +281,97 @@ class PlaywrightBrowser:
|
|
278
281
|
"[PYTHON] Setting up browser window tracking with viewport-only adjustment"
|
279
282
|
)
|
280
283
|
|
284
|
+
# Create resize tracker instance
|
285
|
+
self.resize_tracker = ResizeTracker(self.page)
|
286
|
+
|
281
287
|
# Start polling loop that tracks browser window changes and adjusts viewport only
|
282
288
|
asyncio.create_task(self._track_browser_adjust_viewport())
|
283
289
|
|
284
290
|
async def _track_browser_adjust_viewport(self) -> None:
|
285
291
|
"""Track browser window changes and adjust viewport accordingly.
|
286
292
|
|
287
|
-
This method polls for changes in the browser window size
|
288
|
-
|
289
|
-
while ensuring the content area matches the sketch requirements.
|
293
|
+
This method polls for changes in the browser window size using the
|
294
|
+
ResizeTracker and handles any errors that occur.
|
290
295
|
"""
|
291
|
-
|
292
|
-
|
293
|
-
|
296
|
+
if self.resize_tracker is None:
|
297
|
+
print("[PYTHON] Cannot start tracking: resize_tracker is None")
|
298
|
+
return
|
294
299
|
|
295
300
|
while not self._should_exit.is_set():
|
296
301
|
try:
|
297
|
-
|
302
|
+
# Wait between polls
|
303
|
+
await asyncio.sleep(0.25) # Poll every 250ms
|
298
304
|
|
299
|
-
if
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
305
|
+
# Check if page is still alive
|
306
|
+
if self.page is None or self.page.is_closed():
|
307
|
+
print("[PYTHON] Page closed, signaling exit")
|
308
|
+
self._should_exit.set()
|
309
|
+
return
|
310
|
+
|
311
|
+
# Update resize tracking
|
312
|
+
result = await self.resize_tracker.update()
|
313
|
+
|
314
|
+
if result is not None:
|
315
|
+
assert isinstance(result, Exception)
|
316
|
+
# An exception occurred in resize tracking
|
317
|
+
error_message = str(result)
|
318
|
+
warnings.warn(f"[PYTHON] Error in resize tracking: {error_message}")
|
319
|
+
|
320
|
+
# Be EXTREMELY conservative about browser close detection
|
321
|
+
# Only trigger shutdown on very specific errors that definitively indicate browser closure
|
322
|
+
browser_definitely_closed = any(
|
323
|
+
phrase in error_message.lower()
|
324
|
+
for phrase in [
|
325
|
+
"browser has been closed",
|
326
|
+
"target closed",
|
327
|
+
"connection closed",
|
328
|
+
"target page, probably because the page has been closed",
|
329
|
+
"page has been closed",
|
330
|
+
"browser context has been closed",
|
331
|
+
]
|
319
332
|
)
|
320
333
|
|
321
|
-
#
|
334
|
+
# Also check actual browser state before deciding to shut down
|
335
|
+
browser_state_indicates_closed = False
|
322
336
|
try:
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
screenY: window.screenY
|
330
|
-
})
|
331
|
-
"""
|
332
|
-
)
|
333
|
-
|
334
|
-
print(
|
335
|
-
f"[PYTHON] Window: {window_info['outerWidth']}x{window_info['outerHeight']} at ({window_info['screenX']}, {window_info['screenY']})"
|
336
|
-
)
|
337
|
-
|
338
|
-
except Exception as e:
|
337
|
+
if self.browser and hasattr(self.browser, "is_closed"):
|
338
|
+
browser_state_indicates_closed = self.browser.is_closed()
|
339
|
+
elif self.context and hasattr(self.context, "closed"):
|
340
|
+
browser_state_indicates_closed = self.context.closed
|
341
|
+
except Exception:
|
342
|
+
# If we can't check the state, don't assume it's closed
|
339
343
|
warnings.warn(
|
340
|
-
f"[PYTHON] Could not
|
344
|
+
f"[PYTHON] Could not check browser state: {result}. Assuming browser is not closed."
|
341
345
|
)
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
() => {
|
356
|
-
return {
|
357
|
-
userAgent: navigator.userAgent,
|
358
|
-
platform: navigator.platform,
|
359
|
-
cookieEnabled: navigator.cookieEnabled,
|
360
|
-
language: navigator.language
|
361
|
-
};
|
362
|
-
}
|
363
|
-
"""
|
364
|
-
)
|
365
|
-
|
366
|
-
if browser_info:
|
367
|
-
pass # We have browser info, but don't need to print it constantly
|
346
|
+
browser_state_indicates_closed = False
|
347
|
+
|
348
|
+
if browser_definitely_closed or browser_state_indicates_closed:
|
349
|
+
if browser_definitely_closed:
|
350
|
+
print(
|
351
|
+
f'[PYTHON] Browser has been closed because "{error_message}" matched one of the error phrases or browser state indicates closed, shutting down gracefully...'
|
352
|
+
)
|
353
|
+
elif browser_state_indicates_closed:
|
354
|
+
print(
|
355
|
+
"[PYTHON] Browser state indicates closed, shutting down gracefully..."
|
356
|
+
)
|
357
|
+
self._should_exit.set()
|
358
|
+
return
|
368
359
|
else:
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
360
|
+
# For other errors, just log and continue - don't shut down
|
361
|
+
print(
|
362
|
+
f"[PYTHON] Recoverable error in resize tracking: {result}"
|
363
|
+
)
|
364
|
+
# Add a small delay to prevent tight error loops
|
365
|
+
await asyncio.sleep(1.0)
|
373
366
|
|
374
367
|
except Exception as e:
|
375
368
|
error_message = str(e)
|
376
|
-
warnings.warn(
|
377
|
-
|
378
|
-
# Only trigger shutdown on very specific errors that definitively indicate browser closure
|
379
|
-
browser_definitely_closed = any(
|
380
|
-
phrase in error_message.lower()
|
381
|
-
for phrase in [
|
382
|
-
"browser has been closed",
|
383
|
-
"target closed",
|
384
|
-
"connection closed",
|
385
|
-
"target page, probably because the page has been closed",
|
386
|
-
# "execution context was destroyed",
|
387
|
-
"page has been closed",
|
388
|
-
"browser context has been closed",
|
389
|
-
]
|
369
|
+
warnings.warn(
|
370
|
+
f"[PYTHON] Unexpected error in browser tracking loop: {error_message}"
|
390
371
|
)
|
372
|
+
# Add a small delay to prevent tight error loops
|
373
|
+
await asyncio.sleep(1.0)
|
391
374
|
|
392
|
-
# Also check actual browser state before deciding to shut down
|
393
|
-
browser_state_indicates_closed = False
|
394
|
-
try:
|
395
|
-
if self.browser and hasattr(self.browser, "is_closed"):
|
396
|
-
browser_state_indicates_closed = self.browser.is_closed()
|
397
|
-
elif self.context and hasattr(self.context, "closed"):
|
398
|
-
browser_state_indicates_closed = self.context.closed
|
399
|
-
except Exception:
|
400
|
-
# If we can't check the state, don't assume it's closed
|
401
|
-
warnings.warn(
|
402
|
-
f"[PYTHON] Could not check browser state: {e}. Assuming browser is not closed."
|
403
|
-
)
|
404
|
-
browser_state_indicates_closed = False
|
405
|
-
|
406
|
-
if browser_definitely_closed or browser_state_indicates_closed:
|
407
|
-
if browser_definitely_closed:
|
408
|
-
print(
|
409
|
-
f'[PYTHON] Browser has been closed because "{error_message}" matched one of the error phrases or browser state indicates closed, shutting down gracefully...'
|
410
|
-
)
|
411
|
-
elif browser_state_indicates_closed:
|
412
|
-
print(
|
413
|
-
"[PYTHON] Browser state indicates closed, shutting down gracefully..."
|
414
|
-
)
|
415
|
-
self._should_exit.set()
|
416
|
-
break
|
417
|
-
else:
|
418
|
-
# For other errors, just log and continue - don't shut down
|
419
|
-
print(f"[PYTHON] Recoverable error in browser tracking: {e}")
|
420
|
-
# Add a small delay to prevent tight error loops
|
421
|
-
await asyncio.sleep(1.0)
|
422
|
-
continue
|
423
375
|
warnings.warn("[PYTHON] Browser tracking loop exited.")
|
424
376
|
|
425
377
|
async def wait_for_close(self) -> None:
|
@@ -445,6 +397,8 @@ class PlaywrightBrowser:
|
|
445
397
|
self._should_exit.set()
|
446
398
|
|
447
399
|
try:
|
400
|
+
# Clean up resize tracker
|
401
|
+
self.resize_tracker = None
|
448
402
|
|
449
403
|
if self.page:
|
450
404
|
await self.page.close()
|
@@ -487,6 +441,7 @@ class PlaywrightBrowser:
|
|
487
441
|
_thread.interrupt_main()
|
488
442
|
except Exception as e:
|
489
443
|
print(f"[PYTHON] Error closing Playwright browser: {e}")
|
444
|
+
self._should_exit.set()
|
490
445
|
|
491
446
|
|
492
447
|
def run_playwright_browser(
|
@@ -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
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"
|