screenoverlay 0.5.0__tar.gz → 0.6.1__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: screenoverlay
3
- Version: 0.5.0
3
+ Version: 0.6.1
4
4
  Summary: Cross-platform screen overlay with blur, black, white, and custom modes
5
5
  Home-page: https://github.com/pekay-ai/screenoverlay
6
6
  Author: Pekay
@@ -5,7 +5,7 @@ Provides blur, black, white, and custom color overlays with minimal latency
5
5
 
6
6
  from .overlay import NativeBlurOverlay as Overlay
7
7
 
8
- __version__ = '0.5.0'
8
+ __version__ = '0.6.0'
9
9
  __author__ = 'Pekay'
10
10
  __all__ = ['Overlay']
11
11
 
@@ -2,17 +2,14 @@
2
2
  """
3
3
  Native Blur Overlay - Uses OS-native blur effects
4
4
  No screen capture, no permissions needed, instant appearance
5
+
6
+ Single-process architecture using Tkinter's update() for non-blocking operation.
5
7
  """
6
8
 
7
9
  import tkinter as tk
8
10
  import platform
9
11
  import sys
10
12
  import os
11
- import threading
12
- from multiprocessing import Process, Queue
13
- import time
14
- import atexit
15
- import signal
16
13
 
17
14
  # Try to import screeninfo for multi-monitor support
18
15
  try:
@@ -65,154 +62,170 @@ class NativeBlurOverlay:
65
62
 
66
63
  self.root = None
67
64
  self.windows = [] # List to hold multiple windows for multi-monitor
68
- self._timer_id = None
69
- self._process = None
70
- self._command_queue = None
71
-
72
- # Register cleanup on exit to prevent orphaned processes
73
- atexit.register(self._cleanup_on_exit)
74
-
75
- def _cleanup_on_exit(self):
76
- """Cleanup overlay process on program exit"""
77
- if self._process is not None and self._process.is_alive():
78
- try:
79
- # Try graceful stop first
80
- if self._command_queue is not None:
81
- try:
82
- self._command_queue.put('stop')
83
- except:
84
- pass
85
-
86
- # Wait briefly
87
- self._process.join(timeout=0.5)
88
-
89
- # Force kill if still alive
90
- if self._process.is_alive():
91
- self._process.terminate()
92
- self._process.join(timeout=0.5)
93
-
94
- # Last resort - force kill
95
- if self._process.is_alive():
96
- self._process.kill()
97
- except:
98
- pass
65
+ self._is_visible = False
66
+ self._last_update_time = 0 # Throttle update() calls
99
67
 
100
68
  def start(self):
101
69
  """
102
- Start the overlay process with show/hide control.
70
+ Initialize the overlay windows.
103
71
  Call this once at app startup.
104
72
 
105
- After calling start(), use show() and hide() to control visibility instantly.
73
+ After calling start(), use show() and hide() to control visibility instantly,
74
+ and call update() regularly in your main loop to keep the overlay responsive.
106
75
 
107
- Example for ScreenStop:
76
+ Example:
108
77
  overlay = Overlay(mode='blur', blur_strength=4)
109
78
  overlay.start() # Initialize (call once)
110
79
 
111
- overlay.show() # Show overlay (instant)
112
- time.sleep(2)
113
- overlay.hide() # Hide overlay (instant)
114
- overlay.show() # Show again
80
+ while True:
81
+ overlay.show() # Show overlay (instant)
82
+ time.sleep(2)
83
+ overlay.hide() # Hide overlay (instant)
84
+ overlay.update() # Keep overlay responsive (call regularly!)
115
85
 
116
86
  overlay.stop() # Cleanup when done
117
87
  """
118
- if self._process is not None:
119
- return
88
+ if self.root is not None:
89
+ return # Already started
120
90
 
121
- self._command_queue = Queue()
122
- self._process = Process(target=self._run_process, args=(self._command_queue,), daemon=True)
123
- self._process.start()
91
+ # Create windows for all monitors
92
+ self._create_windows()
124
93
 
125
- # Wait a bit for process to initialize
126
- time.sleep(0.3)
94
+ # Hide all windows initially
95
+ for win in self.windows:
96
+ win.withdraw()
97
+
98
+ self._is_visible = False
127
99
 
128
100
  def show(self):
129
- """Show the overlay (instant, ~1ms)"""
130
- if self._command_queue is not None:
131
- self._command_queue.put('show')
101
+ """Show the overlay (instant, <1ms)"""
102
+ import traceback
103
+ print(f"\n🔴 OVERLAY.SHOW() CALLED! Stack trace:")
104
+ traceback.print_stack()
105
+
106
+ if self.root is None:
107
+ # Auto-start if not started yet
108
+ self.start()
109
+
110
+ if not self._is_visible:
111
+ for win in self.windows:
112
+ try:
113
+ win.deiconify()
114
+ win.attributes('-topmost', True) # Re-enable topmost when showing
115
+ win.lift()
116
+ except Exception as e:
117
+ print(f"Warning: Failed to show window: {e}")
118
+ self._is_visible = True
119
+ print(f"✅ OVERLAY IS NOW VISIBLE\n")
132
120
 
133
121
  def hide(self):
134
- """
135
- Hide the overlay and restart for next use (100% reliable, prevents ghost windows)
122
+ """Hide the overlay by DESTROYING and RECREATING it (prevents ghost windows/CPU leaks)"""
123
+ import traceback
124
+ print(f"\n⚪ OVERLAY.HIDE() CALLED! Stack trace:")
125
+ traceback.print_stack()
136
126
 
137
- This method:
138
- 1. Hides the overlay instantly (user sees clear screen ~1ms)
139
- 2. Stops the overlay process completely (kills any ghost windows)
140
- 3. Starts a fresh overlay ready for next show() (happens in background ~300ms)
127
+ if self.root is None:
128
+ return # Not started yet
141
129
 
142
- This "restart on hide" approach guarantees zero ghost windows by giving
143
- each detection cycle a fresh overlay process, at the cost of ~300ms
144
- startup time that happens during safe periods (when screen is clear).
145
- """
146
- if self._command_queue is not None:
147
- # Hide first (instant for user)
148
- self._command_queue.put('hide')
130
+ if self._is_visible:
131
+ # COMPLETE DESTRUCTION - no lingering windows or events!
132
+ print(f"🗑️ DESTROYING overlay completely...")
133
+ try:
134
+ # Just destroy root - it will destroy all child windows automatically
135
+ if self.root:
136
+ try:
137
+ self.root.destroy()
138
+ except Exception as e:
139
+ print(f"Warning: destroy() failed: {e} (might already be destroyed)")
140
+ except Exception as e:
141
+ print(f"Warning: Failed to destroy overlay: {e}")
142
+
143
+ self.root = None
144
+ self.windows = []
145
+ self._is_visible = False
146
+ print(f"✅ OVERLAY DESTROYED\n")
147
+
148
+ # SAFETY CHECK: Verify system is clean before recreating
149
+ print(f"🔍 SAFETY CHECK: Verifying clean state...")
150
+ if self.root is not None:
151
+ print(f"⚠️ WARNING: self.root is not None after destroy! Force clearing...")
152
+ self.root = None
153
+ if len(self.windows) > 0:
154
+ print(f"⚠️ WARNING: self.windows has {len(self.windows)} entries after destroy! Force clearing...")
155
+ self.windows = []
156
+ if self._is_visible:
157
+ print(f"⚠️ WARNING: _is_visible is True after destroy! Force clearing...")
158
+ self._is_visible = False
149
159
 
150
- # Stop the process completely to prevent ghost windows
151
- self.stop()
160
+ # Small delay to let Tkinter/macOS fully cleanup
161
+ import time
162
+ time.sleep(0.01) # 10ms delay for cleanup
163
+ print(f"✅ SYSTEM CLEAN\n")
152
164
 
153
- # Start fresh overlay for next show() (happens in background)
165
+ # RECREATE FRESH for next show()
166
+ print(f"♻️ RECREATING fresh overlay...")
154
167
  self.start()
168
+ print(f"✅ FRESH OVERLAY READY (hidden)\n")
169
+
170
+ def update(self):
171
+ """
172
+ Keep overlay responsive - call this regularly in your main loop!
173
+
174
+ This processes Tkinter events and keeps the windows responsive.
175
+ Without calling this, the overlay will freeze.
176
+
177
+ Example:
178
+ while True:
179
+ detect_something()
180
+ if detected:
181
+ overlay.show()
182
+ else:
183
+ overlay.hide()
184
+ overlay.update() # ← Call this every loop iteration!
185
+ time.sleep(0.1)
186
+ """
187
+ if self.root is not None:
188
+ try:
189
+ import time
190
+ current_time = time.time()
191
+
192
+ # Throttle: only update every 100ms (10 FPS) to reduce CPU load
193
+ # This prevents excessive event processing while keeping UI responsive
194
+ if current_time - self._last_update_time < 0.1:
195
+ return # Skip this update
196
+
197
+ self._last_update_time = current_time
198
+
199
+ # Defensive check: verify window state matches _is_visible flag
200
+ for win in self.windows:
201
+ try:
202
+ actual_state = win.winfo_viewable()
203
+ if actual_state and not self._is_visible:
204
+ print(f"⚠️ BUG DETECTED: Window is visible but _is_visible=False! Force hiding...")
205
+ win.attributes('-topmost', False)
206
+ win.withdraw()
207
+ elif not actual_state and self._is_visible:
208
+ print(f"⚠️ BUG DETECTED: Window is hidden but _is_visible=True! Syncing flag...")
209
+ self._is_visible = False
210
+ except Exception as e:
211
+ pass # Ignore errors in defensive check
212
+
213
+ # Process Tkinter events
214
+ self.root.update()
215
+ except Exception as e:
216
+ print(f"Warning: Update failed: {e}")
155
217
 
156
218
  def stop(self):
157
219
  """Stop and cleanup the overlay completely"""
158
- if self._command_queue is not None:
159
- self._command_queue.put('stop')
160
-
161
- if self._process is not None:
162
- self._process.join(timeout=2.0)
163
- if self._process.is_alive():
164
- self._process.terminate()
165
- self._process = None
166
-
167
- self._command_queue = None
168
-
169
- def _run_process(self, command_queue):
170
- """Run overlay in separate process with command queue"""
171
- try:
172
- # Create windows for all monitors
173
- self._create_windows()
174
-
175
- # Hide all windows initially
176
- for win in self.windows:
177
- win.withdraw()
178
-
179
- # Process commands from queue
180
- def check_commands():
181
- try:
182
- while not command_queue.empty():
183
- cmd = command_queue.get_nowait()
184
- if cmd == 'show':
185
- for win in self.windows:
186
- try:
187
- win.deiconify()
188
- win.lift()
189
- except Exception as e:
190
- print(f"Warning: Failed to show window: {e}")
191
- elif cmd == 'hide':
192
- for win in self.windows:
193
- try:
194
- win.withdraw()
195
- except Exception as e:
196
- print(f"Warning: Failed to hide window: {e}")
197
- elif cmd == 'stop':
198
- self.root.quit()
199
- return
200
- except Exception as e:
201
- print(f"Warning: Command queue error: {e}")
202
-
203
- # Check again in 10ms
204
- self.root.after(10, check_commands)
205
-
206
- # Start command checker
207
- check_commands()
208
-
209
- # Run mainloop
210
- self.root.mainloop()
211
-
212
- except Exception as e:
213
- print(f"Overlay process error: {e}")
214
- finally:
215
- os._exit(0)
220
+ if self.root is not None:
221
+ try:
222
+ self.root.quit()
223
+ self.root.destroy()
224
+ except:
225
+ pass
226
+ self.root = None
227
+ self.windows = []
228
+ self._is_visible = False
216
229
 
217
230
  def _get_monitors(self):
218
231
  """Get information about all monitors"""
@@ -274,52 +287,9 @@ class NativeBlurOverlay:
274
287
  if self.apply_blur:
275
288
  self._apply_native_blur_to_window(window)
276
289
 
277
- # Bind escape key to exit (only on primary window)
290
+ # Bind escape key to hide (only on primary window)
278
291
  if window == self.root:
279
- window.bind('<Escape>', lambda e: self.kill_completely())
280
- window.focus_set()
281
-
282
- def _create_window(self):
283
- """Internal method to create and configure the Tkinter window"""
284
- self.root = tk.Tk()
285
-
286
- # Remove window decorations
287
- self.root.overrideredirect(True)
288
- self.root.attributes('-topmost', True)
289
-
290
- # Set background color (tint)
291
- bg_color = f'#{self.color_tint[0]:02x}{self.color_tint[1]:02x}{self.color_tint[2]:02x}'
292
- self.root.configure(bg=bg_color)
293
-
294
- # Set opacity
295
- self.root.attributes('-alpha', self.opacity)
296
-
297
- # Full screen
298
- screen_width = self.root.winfo_screenwidth()
299
- screen_height = self.root.winfo_screenheight()
300
- self.root.geometry(f"{screen_width}x{screen_height}+0+0")
301
-
302
- # Apply native blur effect based on OS (only if mode is 'blur')
303
- if self.apply_blur:
304
- self._apply_native_blur()
305
-
306
- # Bind escape key to exit
307
- self.root.bind('<Escape>', lambda e: self.kill_completely())
308
- self.root.focus_set()
309
-
310
- def activate(self, duration=5):
311
- """Show native blur overlay and exit after duration"""
312
- self._create_windows() # Use multi-monitor aware method
313
-
314
- # Auto-exit timer
315
- self._timer_id = self.root.after(int(duration * 1000), self.kill_completely)
316
-
317
- # Show window
318
- self.root.mainloop()
319
-
320
- def _apply_native_blur(self):
321
- """Apply OS-native backdrop blur effect to root window (legacy method)"""
322
- self._apply_native_blur_to_window(self.root)
292
+ window.bind('<Escape>', lambda e: self.hide())
323
293
 
324
294
  def _apply_native_blur_to_window(self, window):
325
295
  """Apply OS-native backdrop blur effect to a specific window"""
@@ -331,10 +301,6 @@ class NativeBlurOverlay:
331
301
  self._apply_windows_blur_to_window(window)
332
302
  elif system == 'Linux':
333
303
  self._apply_linux_blur_to_window(window)
334
-
335
- def _apply_macos_blur(self):
336
- """Apply macOS NSVisualEffectView blur (legacy method)"""
337
- self._apply_macos_blur_to_window(self.root)
338
304
 
339
305
  def _apply_macos_blur_to_window(self, window):
340
306
  """Apply macOS NSVisualEffectView blur to a specific window"""
@@ -359,17 +325,17 @@ class NativeBlurOverlay:
359
325
  from Cocoa import NSMakeRect
360
326
 
361
327
  # Get all windows and find ours
362
- for window in NSApp.windows():
363
- if window.isVisible():
328
+ for ns_window in NSApp.windows():
329
+ if ns_window.isVisible():
364
330
  # Create visual effect view
365
- frame = window.contentView().frame()
331
+ frame = ns_window.contentView().frame()
366
332
  effect_view = NSVisualEffectView.alloc().initWithFrame_(frame)
367
333
  effect_view.setBlendingMode_(NSVisualEffectBlendingModeBehindWindow)
368
334
  effect_view.setMaterial_(NSVisualEffectMaterialDark)
369
335
  effect_view.setState_(1) # Active state
370
336
 
371
337
  # Add as subview
372
- window.contentView().addSubview_positioned_relativeTo_(
338
+ ns_window.contentView().addSubview_positioned_relativeTo_(
373
339
  effect_view, 0, None
374
340
  )
375
341
  break
@@ -380,10 +346,6 @@ class NativeBlurOverlay:
380
346
  print("pyobjc not available, install with: pip install pyobjc-framework-Cocoa")
381
347
  except Exception as e:
382
348
  print(f"macOS blur effect failed: {e}")
383
-
384
- def _apply_windows_blur(self):
385
- """Apply Windows Acrylic/Blur effect (legacy method)"""
386
- self._apply_windows_blur_to_window(self.root)
387
349
 
388
350
  def _apply_windows_blur_to_window(self, window):
389
351
  """Apply Windows Acrylic/Blur effect to a specific window"""
@@ -446,10 +408,6 @@ class NativeBlurOverlay:
446
408
  # Blur effect failed, but window will still work (just without blur)
447
409
  print(f"Note: Windows blur effect unavailable: {e}")
448
410
  print("Overlay will work but without native blur effect")
449
-
450
- def _apply_linux_blur(self):
451
- """Apply Linux compositor blur (X11/Wayland) (legacy method)"""
452
- self._apply_linux_blur_to_window(self.root)
453
411
 
454
412
  def _apply_linux_blur_to_window(self, window):
455
413
  """Apply Linux compositor blur (X11/Wayland) to a specific window"""
@@ -465,18 +423,31 @@ class NativeBlurOverlay:
465
423
  except Exception as e:
466
424
  print(f"Linux blur effect hint failed: {e}")
467
425
 
468
- def kill_completely(self):
469
- """Exit the overlay completely (for activate() backward compatibility)"""
470
- try:
471
- if self.root:
472
- self.root.quit()
473
- self.root.destroy()
474
- except:
475
- pass
426
+ # Backward compatibility methods
427
+ def activate(self, duration=5):
428
+ """
429
+ Show overlay for a fixed duration and then exit (blocking).
430
+
431
+ This is the legacy API for backward compatibility.
432
+ For new code, use start() + show()/hide() + update() instead.
433
+ """
434
+ self.start()
435
+ self.show()
436
+
437
+ # Schedule hide and cleanup
438
+ self.root.after(int(duration * 1000), self._deactivate_and_exit)
476
439
 
477
- # Only call os._exit if we're in activate() mode (has timer)
478
- if self._timer_id is not None:
479
- os._exit(0)
440
+ # Run mainloop (blocking)
441
+ self.root.mainloop()
442
+
443
+ def _deactivate_and_exit(self):
444
+ """Helper for activate() - hide and exit"""
445
+ self.hide()
446
+ self.stop()
447
+
448
+
449
+ # Alias for convenience
450
+ Overlay = NativeBlurOverlay
480
451
 
481
452
 
482
453
  if __name__ == "__main__":
@@ -490,20 +461,19 @@ if __name__ == "__main__":
490
461
 
491
462
  print(f"Testing mode='{mode}' for 3 seconds...")
492
463
  print("Available modes: blur, black, white, custom")
493
- print("Usage: python NativeBlurOverlay.py [mode]")
464
+ print("Usage: python overlay.py [mode]")
494
465
  print()
495
466
 
496
467
  if mode == 'blur':
497
- overlay = NativeBlurOverlay(mode='blur', blur_strength=4)
468
+ overlay = Overlay(mode='blur', blur_strength=4)
498
469
  elif mode == 'black':
499
- overlay = NativeBlurOverlay(mode='black')
470
+ overlay = Overlay(mode='black')
500
471
  elif mode == 'white':
501
- overlay = NativeBlurOverlay(mode='white')
472
+ overlay = Overlay(mode='white')
502
473
  elif mode == 'custom':
503
- overlay = NativeBlurOverlay(mode='custom', opacity=0.7, color_tint=(255, 0, 0)) # Red example
474
+ overlay = Overlay(mode='custom', opacity=0.7, color_tint=(255, 0, 0)) # Red example
504
475
  else:
505
476
  print(f"Unknown mode: {mode}")
506
477
  sys.exit(1)
507
478
 
508
479
  overlay.activate(duration=3)
509
-
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: screenoverlay
3
- Version: 0.5.0
3
+ Version: 0.6.1
4
4
  Summary: Cross-platform screen overlay with blur, black, white, and custom modes
5
5
  Home-page: https://github.com/pekay-ai/screenoverlay
6
6
  Author: Pekay
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setup(
7
7
  name="screenoverlay",
8
- version="0.5.0",
8
+ version="0.6.1",
9
9
  author="Pekay",
10
10
  author_email="ppnicky@gmail.com",
11
11
  description="Cross-platform screen overlay with blur, black, white, and custom modes",
File without changes
File without changes
File without changes