fastled 1.4.31__py3-none-any.whl → 1.4.33__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/client_server.py +46 -15
- 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 +67 -160
- fastled/playwright/resize_tracking.py +127 -0
- fastled/print_filter.py +52 -52
- fastled/string_diff.py +165 -165
- fastled/types.py +4 -0
- fastled/version.py +41 -41
- fastled/web_compile.py +80 -5
- {fastled-1.4.31.dist-info → fastled-1.4.33.dist-info}/METADATA +531 -531
- {fastled-1.4.31.dist-info → fastled-1.4.33.dist-info}/RECORD +26 -18
- {fastled-1.4.31.dist-info → fastled-1.4.33.dist-info}/WHEEL +0 -0
- {fastled-1.4.31.dist-info → fastled-1.4.33.dist-info}/entry_points.txt +0 -0
- {fastled-1.4.31.dist-info → fastled-1.4.33.dist-info}/licenses/LICENSE +0 -0
- {fastled-1.4.31.dist-info → fastled-1.4.33.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,50 +281,26 @@ 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
|
-
async def _get_window_info(self) -> dict[str, int] | None:
|
285
|
-
"""Get browser window dimensions information.
|
286
|
-
|
287
|
-
Returns:
|
288
|
-
Dictionary containing window dimensions or None if unable to retrieve
|
289
|
-
"""
|
290
|
-
if self.page is None:
|
291
|
-
return None
|
292
|
-
|
293
|
-
try:
|
294
|
-
return await self.page.evaluate(
|
295
|
-
"""
|
296
|
-
() => {
|
297
|
-
return {
|
298
|
-
outerWidth: window.outerWidth,
|
299
|
-
outerHeight: window.outerHeight,
|
300
|
-
innerWidth: window.innerWidth,
|
301
|
-
innerHeight: window.innerHeight,
|
302
|
-
contentWidth: document.documentElement.clientWidth,
|
303
|
-
contentHeight: document.documentElement.clientHeight
|
304
|
-
};
|
305
|
-
}
|
306
|
-
"""
|
307
|
-
)
|
308
|
-
except Exception:
|
309
|
-
return None
|
310
|
-
|
311
290
|
async def _track_browser_adjust_viewport(self) -> None:
|
312
291
|
"""Track browser window changes and adjust viewport accordingly.
|
313
292
|
|
314
|
-
This method polls for changes in the browser window size
|
315
|
-
|
316
|
-
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.
|
317
295
|
"""
|
318
|
-
|
319
|
-
|
296
|
+
if self.resize_tracker is None:
|
297
|
+
print("[PYTHON] Cannot start tracking: resize_tracker is None")
|
298
|
+
return
|
320
299
|
|
321
300
|
while not self._should_exit.is_set():
|
322
301
|
try:
|
323
|
-
# Wait
|
324
|
-
await asyncio.sleep(0.25) # Poll every
|
302
|
+
# Wait between polls
|
303
|
+
await asyncio.sleep(0.25) # Poll every 250ms
|
325
304
|
|
326
305
|
# Check if page is still alive
|
327
306
|
if self.page is None or self.page.is_closed():
|
@@ -329,144 +308,70 @@ class PlaywrightBrowser:
|
|
329
308
|
self._should_exit.set()
|
330
309
|
return
|
331
310
|
|
332
|
-
#
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
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
|
+
]
|
332
|
+
)
|
337
333
|
|
338
|
-
|
339
|
-
|
340
|
-
|
334
|
+
# Also check actual browser state before deciding to shut down
|
335
|
+
browser_state_indicates_closed = False
|
336
|
+
try:
|
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
|
343
|
+
warnings.warn(
|
344
|
+
f"[PYTHON] Could not check browser state: {result}. Assuming browser is not closed."
|
341
345
|
)
|
346
|
+
browser_state_indicates_closed = False
|
342
347
|
|
343
|
-
|
344
|
-
if
|
345
|
-
|
346
|
-
if last_outer_size is not None:
|
347
|
-
print("[PYTHON] *** BROWSER WINDOW RESIZED ***")
|
348
|
-
print(
|
349
|
-
f"[PYTHON] Outer window changed from {last_outer_size[0]}x{last_outer_size[1]} to {current_outer[0]}x{current_outer[1]}"
|
350
|
-
)
|
351
|
-
|
352
|
-
last_outer_size = current_outer
|
353
|
-
|
354
|
-
# Set viewport to match the outer window size
|
355
|
-
outer_width = int(window_info["outerWidth"])
|
356
|
-
outer_height = int(window_info["outerHeight"])
|
357
|
-
|
348
|
+
if browser_definitely_closed or browser_state_indicates_closed:
|
349
|
+
if browser_definitely_closed:
|
358
350
|
print(
|
359
|
-
f
|
351
|
+
f'[PYTHON] Browser has been closed because "{error_message}" matched one of the error phrases or browser state indicates closed, shutting down gracefully...'
|
360
352
|
)
|
361
|
-
|
362
|
-
|
363
|
-
|
353
|
+
elif browser_state_indicates_closed:
|
354
|
+
print(
|
355
|
+
"[PYTHON] Browser state indicates closed, shutting down gracefully..."
|
364
356
|
)
|
365
|
-
|
366
|
-
|
367
|
-
# Wait briefly for browser to settle after viewport change
|
368
|
-
# await asyncio.sleep(2)
|
369
|
-
|
370
|
-
# Query the actual window dimensions after the viewport change
|
371
|
-
updated_window_info = await self._get_window_info()
|
372
|
-
|
373
|
-
if updated_window_info:
|
374
|
-
print(
|
375
|
-
f"[PYTHON] Updated window info: {updated_window_info}"
|
376
|
-
)
|
377
|
-
|
378
|
-
# Update our tracking with the actual final outer size
|
379
|
-
last_outer_size = (
|
380
|
-
updated_window_info["outerWidth"],
|
381
|
-
updated_window_info["outerHeight"],
|
382
|
-
)
|
383
|
-
print(
|
384
|
-
f"[PYTHON] Updated last_outer_size to actual final size: {last_outer_size}"
|
385
|
-
)
|
386
|
-
else:
|
387
|
-
print("[PYTHON] Could not get updated window info")
|
388
|
-
|
389
|
-
except Exception as e:
|
390
|
-
warnings.warn(
|
391
|
-
f"[PYTHON] Could not get browser window info: {e}. Assuming browser is not closed."
|
392
|
-
)
|
393
|
-
import traceback
|
394
|
-
|
395
|
-
traceback.print_exc()
|
396
|
-
pass
|
397
|
-
|
398
|
-
# Get the browser window information periodically
|
399
|
-
try:
|
400
|
-
browser_info = await self.page.evaluate(
|
401
|
-
"""
|
402
|
-
() => {
|
403
|
-
return {
|
404
|
-
userAgent: navigator.userAgent,
|
405
|
-
platform: navigator.platform,
|
406
|
-
cookieEnabled: navigator.cookieEnabled,
|
407
|
-
language: navigator.language
|
408
|
-
};
|
409
|
-
}
|
410
|
-
"""
|
411
|
-
)
|
412
|
-
|
413
|
-
if browser_info:
|
414
|
-
pass # We have browser info, but don't need to print it constantly
|
357
|
+
self._should_exit.set()
|
358
|
+
return
|
415
359
|
else:
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
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)
|
420
366
|
|
421
367
|
except Exception as e:
|
422
368
|
error_message = str(e)
|
423
|
-
warnings.warn(
|
424
|
-
|
425
|
-
# Only trigger shutdown on very specific errors that definitively indicate browser closure
|
426
|
-
browser_definitely_closed = any(
|
427
|
-
phrase in error_message.lower()
|
428
|
-
for phrase in [
|
429
|
-
"browser has been closed",
|
430
|
-
"target closed",
|
431
|
-
"connection closed",
|
432
|
-
"target page, probably because the page has been closed",
|
433
|
-
# "execution context was destroyed",
|
434
|
-
"page has been closed",
|
435
|
-
"browser context has been closed",
|
436
|
-
]
|
369
|
+
warnings.warn(
|
370
|
+
f"[PYTHON] Unexpected error in browser tracking loop: {error_message}"
|
437
371
|
)
|
372
|
+
# Add a small delay to prevent tight error loops
|
373
|
+
await asyncio.sleep(1.0)
|
438
374
|
|
439
|
-
# Also check actual browser state before deciding to shut down
|
440
|
-
browser_state_indicates_closed = False
|
441
|
-
try:
|
442
|
-
if self.browser and hasattr(self.browser, "is_closed"):
|
443
|
-
browser_state_indicates_closed = self.browser.is_closed()
|
444
|
-
elif self.context and hasattr(self.context, "closed"):
|
445
|
-
browser_state_indicates_closed = self.context.closed
|
446
|
-
except Exception:
|
447
|
-
# If we can't check the state, don't assume it's closed
|
448
|
-
warnings.warn(
|
449
|
-
f"[PYTHON] Could not check browser state: {e}. Assuming browser is not closed."
|
450
|
-
)
|
451
|
-
browser_state_indicates_closed = False
|
452
|
-
|
453
|
-
if browser_definitely_closed or browser_state_indicates_closed:
|
454
|
-
if browser_definitely_closed:
|
455
|
-
print(
|
456
|
-
f'[PYTHON] Browser has been closed because "{error_message}" matched one of the error phrases or browser state indicates closed, shutting down gracefully...'
|
457
|
-
)
|
458
|
-
elif browser_state_indicates_closed:
|
459
|
-
print(
|
460
|
-
"[PYTHON] Browser state indicates closed, shutting down gracefully..."
|
461
|
-
)
|
462
|
-
self._should_exit.set()
|
463
|
-
break
|
464
|
-
else:
|
465
|
-
# For other errors, just log and continue - don't shut down
|
466
|
-
print(f"[PYTHON] Recoverable error in browser tracking: {e}")
|
467
|
-
# Add a small delay to prevent tight error loops
|
468
|
-
await asyncio.sleep(1.0)
|
469
|
-
continue
|
470
375
|
warnings.warn("[PYTHON] Browser tracking loop exited.")
|
471
376
|
|
472
377
|
async def wait_for_close(self) -> None:
|
@@ -492,6 +397,8 @@ class PlaywrightBrowser:
|
|
492
397
|
self._should_exit.set()
|
493
398
|
|
494
399
|
try:
|
400
|
+
# Clean up resize tracker
|
401
|
+
self.resize_tracker = None
|
495
402
|
|
496
403
|
if self.page:
|
497
404
|
await self.page.close()
|
@@ -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"
|