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.
@@ -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 and adjusts
288
- the viewport size to match, maintaining the browser window dimensions
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
- last_viewport = None
292
- consecutive_same_count = 0
293
- max_consecutive_same = 5
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
- await asyncio.sleep(0.5) # Poll every 500ms
302
+ # Wait between polls
303
+ await asyncio.sleep(0.25) # Poll every 250ms
298
304
 
299
- if self.page is None:
300
- continue
301
-
302
- # Get current viewport size
303
- current_viewport = await self.page.evaluate(
304
- """
305
- () => ({
306
- width: window.innerWidth,
307
- height: window.innerHeight
308
- })
309
- """
310
- )
311
-
312
- # Check if viewport changed
313
- if current_viewport != last_viewport:
314
- last_viewport = current_viewport
315
- consecutive_same_count = 0
316
-
317
- print(
318
- f"[PYTHON] Viewport: {current_viewport['width']}x{current_viewport['height']}"
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
- # Try to get window outer dimensions for context
334
+ # Also check actual browser state before deciding to shut down
335
+ browser_state_indicates_closed = False
322
336
  try:
323
- window_info = await self.page.evaluate(
324
- """
325
- () => ({
326
- outerWidth: window.outerWidth,
327
- outerHeight: window.outerHeight,
328
- screenX: window.screenX,
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 get browser window info: {e}. Assuming browser is not closed."
344
+ f"[PYTHON] Could not check browser state: {result}. Assuming browser is not closed."
341
345
  )
342
- pass
343
-
344
- else:
345
- consecutive_same_count += 1
346
- if consecutive_same_count >= max_consecutive_same:
347
- # Viewport hasn't changed for a while, reduce polling frequency
348
- await asyncio.sleep(2.0)
349
- consecutive_same_count = 0
350
-
351
- # Get the browser window information periodically
352
- try:
353
- browser_info = await self.page.evaluate(
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
- print("[PYTHON] Could not get browser window info")
370
-
371
- except Exception as e:
372
- print(f"[PYTHON] Could not get browser info: {e}")
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(f"[PYTHON] Error in browser tracking: {error_message}")
377
- # Be EXTREMELY conservative about browser close detection
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"