"""Black Screen Dim — гасит все мониторы чёрным безрамочным окном (Windows). Для КАЖДОГО подключённого монитора создаётся отдельное borderless topmost окно, плотно закрывающее монитор целиком (по его rcMonitor, в device-координатах). Яркость гасится и восстанавливается на ВСЕХ мониторах. ESC (глобально, через low-level keyboard hook) — выход. Подробный лог пишется в %TEMP%\\black_screen_dim.log. """ import ctypes import ctypes.wintypes as wt import logging import os import sys import tempfile import threading import screen_brightness_control as sbc _original_brightness = None LOG = logging.getLogger("black_screen_dim") # -------------------------------------------------------------------------- # Logging # -------------------------------------------------------------------------- def setup_logging(): log_path = os.path.join(tempfile.gettempdir(), "black_screen_dim.log") try: logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.FileHandler(log_path, encoding="utf-8"), logging.StreamHandler(sys.stderr), ], ) except Exception: logging.basicConfig(level=logging.INFO) LOG.info("log file: %s", log_path) return log_path # -------------------------------------------------------------------------- # Brightness — every monitor # -------------------------------------------------------------------------- # -------------------------------------------------------------------------- # Brightness — hybrid: fast native DDC/CI (dxva2) with per-monitor WMI fallback # -------------------------------------------------------------------------- class BrightnessController: """Controls brightness on every monitor. Prefers the native DDC/CI API (dxva2.dll, sub-ms) and falls back per monitor to screen_brightness_control (WMI) for panels that don't expose DDC/CI (e.g. laptop internal screens).""" PHYSICAL_MONITOR = None # ctypes struct, set on Windows def __init__(self): self.targets = [] # [("native", handle, (lo,cur,hi)), ("wmi", idx)] self.saved = None @staticmethod def _native(): return sys.platform == "win32" and hasattr(ctypes.windll, "dxva2") def _open_physical(self, hmon): dx = ctypes.windll.dxva2 arr = (BrightnessController.PHYSICAL_MONITOR * 1)() if not dx.GetPhysicalMonitorsFromHMONITOR(int(hmon), 1, arr): return None return arr[0].hPhysicalMonitor def _read_native(self, handle): dx = ctypes.windll.dxva2 lo = wt.DWORD(); cur = wt.DWORD(); hi = wt.DWORD() if not dx.GetMonitorBrightness(int(handle), ctypes.byref(lo), ctypes.byref(cur), ctypes.byref(hi)): return None return (lo.value, cur.value, hi.value) def _write_native(self, handle, value): try: dx = ctypes.windll.dxva2 res = {} def _t(): try: res["v"] = bool( dx.SetMonitorBrightness(int(handle), wt.DWORD(int(value))) ) except Exception as e: res["e"] = e th = threading.Thread(target=_t, daemon=True) th.start() th.join(timeout=2.0) if th.is_alive(): LOG.warning("native SetMonitorBrightness timed out (monitor in standby?)") return False if "e" in res: LOG.warning("native write failed: %r", res["e"]) return False return res.get("v", False) except Exception as e: LOG.warning("native write error: %r", e) return False def _close_native(self, handle): try: dx = ctypes.windll.dxva2 arr = (BrightnessController.PHYSICAL_MONITOR * 1)() arr[0].hPhysicalMonitor = handle res = {} def _t(): try: res["v"] = dx.DestroyPhysicalMonitors(1, arr) except Exception as e: res["e"] = e th = threading.Thread(target=_t, daemon=True) th.start() th.join(timeout=2.0) if th.is_alive(): return if "e" in res: LOG.warning("native close failed: %r", res["e"]) except Exception as e: LOG.warning("native close error: %r", e) def _build(self): self.targets = [] if self._native(): for i, hmon in enumerate(enum_monitor_handles()): handle = None try: handle = self._open_physical(hmon) except Exception as e: LOG.warning("native open failed: %r", e) handle = None if handle is not None: try: r = self._read_native(handle) except Exception as e: LOG.warning("native read failed: %r", e) r = None if r is not None: self.targets.append(("native", handle, r)) continue try: self._close_native(handle) except Exception: pass self.targets.append(("wmi", i)) if not self.targets: LOG.warning("no monitors via native; brightness control disabled") LOG.info("brightness targets: %s", [t[0] for t in self.targets]) def save(self): self._build() values = [] for t in self.targets: if t[0] == "native": _, handle, (lo, cur, hi) = t values.append(cur) else: idx = t[1] try: v = sbc.get_brightness(display=idx, method="wmi") except Exception as e: LOG.warning("wmi get display %d failed: %r", idx, e) v = None if isinstance(v, list): v = v[0] if v else None values.append(v) self.saved = values LOG.info("saved brightness: %s", values) return values def dim(self): for t in self.targets: if t[0] == "native": _, handle, (lo, cur, hi) = t ok = self._write_native(handle, lo) LOG.info("native dim -> %s (%s)", lo, "ok" if ok else "FAIL") else: idx = t[1] try: sbc.set_brightness(0, display=idx, method="wmi") LOG.info("wmi dim display %d", idx) except Exception as e: LOG.warning("wmi dim display %d failed: %r", idx, e) def restore(self): if self.saved is None: self.close() return for t, val in zip(self.targets, self.saved): if val is None: continue if t[0] == "native": _, handle, _ = t ok = self._write_native(handle, val) LOG.info("native restore %s (%s)", val, "ok" if ok else "FAIL") else: idx = t[1] try: sbc.set_brightness(int(val), display=idx, method="wmi") LOG.info("wmi restore display %d -> %s", idx, val) except Exception as e: LOG.warning("wmi restore display %d failed: %r", idx, e) self.close() def close(self): for t in self.targets: if t[0] == "native": try: self._close_native(t[1]) except Exception: pass # -------------------------------------------------------------------------- # Win32 overlay — one borderless topmost window per monitor # -------------------------------------------------------------------------- if sys.platform == "win32": user32 = ctypes.windll.user32 gdi32 = ctypes.windll.gdi32 kernel32 = ctypes.windll.kernel32 try: shcore = ctypes.windll.shcore except Exception: shcore = None WS_POPUP = 0x80000000 WS_VISIBLE = 0x10000000 WS_EX_TOPMOST = 0x00000008 WS_EX_TOOLWINDOW = 0x00000080 SWP_SHOWWINDOW = 0x0040 HWND_TOPMOST = -1 WM_PAINT = 0x000F WM_ERASEBKGND = 0x0014 WM_DESTROY = 0x0002 WM_CLOSE = 0x0010 WM_TIMER = 0x0113 WM_KEYDOWN_LL = 0x0100 VK_ESCAPE = 0x1B WH_KEYBOARD_LL = 13 DT_CENTER = 0x00000001 DT_VCENTER = 0x00000004 DT_SINGLELINE = 0x00000020 TRANSPARENT = 1 DEFAULT_GUI_FONT = 17 ES_CONTINUOUS = 0x80000000 ES_SYSTEM_REQUIRED = 0x00000001 ES_DISPLAY_REQUIRED = 0x00000002 LRESULT = ctypes.c_int64 HMONITOR = ctypes.c_void_p HHOOK = ctypes.c_void_p HCURSOR = ctypes.c_void_p ULONG_PTR = ctypes.c_void_p UINT_PTR = ctypes.c_void_p WNDPROC = ctypes.WINFUNCTYPE(LRESULT, wt.HWND, wt.UINT, wt.WPARAM, wt.LPARAM) MONITORENUMPROC = ctypes.WINFUNCTYPE( wt.BOOL, HMONITOR, wt.HDC, ctypes.POINTER(wt.RECT), wt.LPARAM ) HOOKPROC = ctypes.WINFUNCTYPE(LRESULT, ctypes.c_int, wt.WPARAM, wt.LPARAM) class WNDCLASSEXW(ctypes.Structure): _fields_ = [ ("cbSize", wt.UINT), ("style", wt.UINT), ("lpfnWndProc", WNDPROC), ("cbClsExtra", wt.INT), ("cbWndExtra", wt.INT), ("hInstance", wt.HINSTANCE), ("hIcon", wt.HICON), ("hCursor", HCURSOR), ("hbrBackground", wt.HBRUSH), ("lpszMenuName", wt.LPCWSTR), ("lpszClassName", wt.LPCWSTR), ("hIconSm", wt.HICON), ] class PAINTSTRUCT(ctypes.Structure): _fields_ = [ ("hdc", wt.HDC), ("fErase", wt.BOOL), ("rcPaint", wt.RECT), ("fRestore", wt.BOOL), ("fIncUpdate", wt.BOOL), ("rgbReserved", wt.BYTE * 32), ] class KBDLLHOOKSTRUCT(ctypes.Structure): _fields_ = [ ("vkCode", wt.DWORD), ("scanCode", wt.DWORD), ("flags", wt.DWORD), ("time", wt.DWORD), ("dwExtraInfo", ULONG_PTR), ] # --- prototypes (64-bit safe) --- user32.EnumDisplayMonitors.argtypes = [wt.HDC, ctypes.POINTER(wt.RECT), MONITORENUMPROC, wt.LPARAM] user32.EnumDisplayMonitors.restype = wt.BOOL user32.RegisterClassExW.argtypes = [ctypes.POINTER(WNDCLASSEXW)] user32.RegisterClassExW.restype = wt.ATOM user32.CreateWindowExW.argtypes = [ wt.DWORD, wt.LPCWSTR, wt.LPCWSTR, wt.DWORD, wt.INT, wt.INT, wt.INT, wt.INT, wt.HWND, wt.HMENU, wt.HINSTANCE, ctypes.c_void_p, ] user32.CreateWindowExW.restype = wt.HWND user32.ShowWindow.argtypes = [wt.HWND, wt.INT] user32.ShowWindow.restype = wt.BOOL user32.UpdateWindow.argtypes = [wt.HWND] user32.UpdateWindow.restype = wt.BOOL user32.SetWindowPos.argtypes = [wt.HWND, wt.HWND, wt.INT, wt.INT, wt.INT, wt.INT, wt.UINT] user32.SetWindowPos.restype = wt.BOOL user32.SetTimer.argtypes = [wt.HWND, UINT_PTR, wt.UINT, ctypes.c_void_p] user32.SetTimer.restype = UINT_PTR user32.KillTimer.argtypes = [wt.HWND, UINT_PTR] user32.KillTimer.restype = wt.BOOL user32.GetClientRect.argtypes = [wt.HWND, ctypes.POINTER(wt.RECT)] user32.GetClientRect.restype = wt.BOOL user32.BeginPaint.argtypes = [wt.HWND, ctypes.POINTER(PAINTSTRUCT)] user32.BeginPaint.restype = wt.HDC user32.EndPaint.argtypes = [wt.HWND, ctypes.POINTER(PAINTSTRUCT)] user32.EndPaint.restype = wt.BOOL user32.InvalidateRect.argtypes = [wt.HWND, ctypes.POINTER(wt.RECT), wt.BOOL] user32.InvalidateRect.restype = wt.BOOL user32.PostQuitMessage.argtypes = [wt.INT] user32.PostQuitMessage.restype = None user32.GetMessageW.argtypes = [ctypes.POINTER(wt.MSG), wt.HWND, wt.UINT, wt.UINT] user32.GetMessageW.restype = wt.LONG user32.TranslateMessage.argtypes = [ctypes.POINTER(wt.MSG)] user32.TranslateMessage.restype = wt.BOOL user32.DispatchMessageW.argtypes = [ctypes.POINTER(wt.MSG)] user32.DispatchMessageW.restype = wt.LONG user32.SetWindowsHookExW.argtypes = [wt.INT, HOOKPROC, wt.HINSTANCE, wt.DWORD] user32.SetWindowsHookExW.restype = HHOOK user32.CallNextHookEx.argtypes = [HHOOK, ctypes.c_int, wt.WPARAM, wt.LPARAM] user32.CallNextHookEx.restype = LRESULT user32.UnhookWindowsHookEx.argtypes = [HHOOK] user32.UnhookWindowsHookEx.restype = wt.BOOL user32.DestroyWindow.argtypes = [wt.HWND] user32.DestroyWindow.restype = wt.BOOL user32.ShowCursor.argtypes = [wt.BOOL] user32.ShowCursor.restype = wt.INT user32.mouse_event.argtypes = [wt.DWORD, wt.DWORD, wt.DWORD, wt.DWORD, ctypes.c_void_p] user32.mouse_event.restype = None user32.DefWindowProcW.argtypes = [wt.HWND, wt.UINT, wt.WPARAM, wt.LPARAM] user32.DefWindowProcW.restype = LRESULT user32.SetProcessDPIAware.argtypes = [] user32.SetProcessDPIAware.restype = wt.BOOL gdi32.CreateSolidBrush.argtypes = [wt.COLORREF] gdi32.DeleteObject.restype = wt.BOOL gdi32.SetTextColor.argtypes = [wt.HDC, wt.COLORREF] gdi32.SetTextColor.restype = wt.COLORREF gdi32.SetBkMode.argtypes = [wt.HDC, wt.INT] gdi32.SetBkMode.restype = wt.INT gdi32.GetStockObject.argtypes = [wt.INT] gdi32.GetStockObject.restype = wt.HGDIOBJ gdi32.SelectObject.argtypes = [wt.HDC, wt.HGDIOBJ] gdi32.SelectObject.restype = wt.HGDIOBJ user32.DrawTextW.argtypes = [wt.HDC, wt.LPCWSTR, wt.INT, ctypes.POINTER(wt.RECT), wt.UINT] user32.DrawTextW.restype = wt.INT kernel32.GetModuleHandleW.argtypes = [wt.LPCWSTR] kernel32.GetModuleHandleW.restype = wt.HINSTANCE kernel32.GetLastError.argtypes = [] kernel32.GetLastError.restype = wt.DWORD kernel32.SetThreadExecutionState.argtypes = [wt.DWORD] kernel32.SetThreadExecutionState.restype = wt.DWORD # --- native DDC/CI brightness (dxva2) --- class PHYSICAL_MONITOR(ctypes.Structure): _fields_ = [ ("hPhysicalMonitor", wt.HANDLE), ("szPhysicalMonitorDescription", ctypes.c_wchar * 128), ] try: dx = ctypes.windll.dxva2 dx.GetPhysicalMonitorsFromHMONITOR.argtypes = [ HMONITOR, wt.DWORD, ctypes.POINTER(PHYSICAL_MONITOR)] dx.GetPhysicalMonitorsFromHMONITOR.restype = wt.BOOL dx.GetMonitorBrightness.argtypes = [ wt.HANDLE, ctypes.POINTER(wt.DWORD), ctypes.POINTER(wt.DWORD), ctypes.POINTER(wt.DWORD)] dx.GetMonitorBrightness.restype = wt.BOOL dx.SetMonitorBrightness.argtypes = [wt.HANDLE, wt.DWORD] dx.SetMonitorBrightness.restype = wt.BOOL dx.DestroyPhysicalMonitors.argtypes = [ wt.DWORD, ctypes.POINTER(PHYSICAL_MONITOR)] dx.DestroyPhysicalMonitors.restype = wt.BOOL BrightnessController.PHYSICAL_MONITOR = PHYSICAL_MONITOR except Exception as e: LOG.warning("dxva2 init failed: %r", e) _hwnds = [] _hint_active = True _hook_id = None _hint_text = "ESC - exit" def enum_monitor_rects(): rects = [] def cb(hmon, hdc, lprect, lparam): r = lprect.contents rects.append((r.left, r.top, r.right, r.bottom)) return True user32.EnumDisplayMonitors(None, None, MONITORENUMPROC(cb), 0) return rects def enum_monitor_handles(): handles = [] def cb(hmon, hdc, lprect, lparam): handles.append(hmon) return True user32.EnumDisplayMonitors(None, None, MONITORENUMPROC(cb), 0) return handles def _wndproc(hwnd, msg, wparam, lparam): global _hint_active if msg == WM_PAINT: # Background (black) is erased by DefWindowProc via the class brush. ps = PAINTSTRUCT() hdc = user32.BeginPaint(hwnd, ctypes.byref(ps)) if _hint_active: gdi32.SetTextColor(hdc, 0xFFFFFF) gdi32.SetBkMode(hdc, TRANSPARENT) font = gdi32.GetStockObject(DEFAULT_GUI_FONT) old = gdi32.SelectObject(hdc, font) buf = ctypes.create_unicode_buffer(_hint_text) user32.DrawTextW( hdc, buf, -1, ctypes.byref(ps.rcPaint), DT_CENTER | DT_VCENTER | DT_SINGLELINE, ) if old: gdi32.SelectObject(hdc, old) user32.EndPaint(hwnd, ctypes.byref(ps)) return 0 if msg == WM_TIMER: _hint_active = False user32.KillTimer(hwnd, 1) for h in _hwnds: user32.InvalidateRect(h, None, True) return 0 if msg in (WM_DESTROY, WM_CLOSE): user32.PostQuitMessage(0) return 0 return user32.DefWindowProcW(hwnd, msg, wparam, lparam) def _low_level_keyboard_proc(nCode, wParam, lParam): if nCode >= 0 and wParam == WM_KEYDOWN_LL: kb = ctypes.cast(lParam, ctypes.POINTER(KBDLLHOOKSTRUCT)).contents if kb.vkCode == VK_ESCAPE: LOG.info("ESC pressed -> exit") user32.PostQuitMessage(0) return user32.CallNextHookEx(_hook_id, nCode, wParam, lParam) def run_overlay(): global _hwnds, _hook_id # Per-monitor V2 DPI awareness: device-coord rects map 1:1 to each # monitor regardless of mixed DPI / where the primary sits. if shcore is not None: try: shcore.SetProcessDpiAwareness(2) # PROCESS_PER_MONITOR_DPI_AWARE_V2 except Exception as e: LOG.warning("SetProcessDpiAwareness failed: %r", e) user32.SetProcessDPIAware() else: user32.SetProcessDPIAware() rects = enum_monitor_rects() LOG.info("monitors found: %s", rects) if not rects: LOG.warning("no monitors enumerated, falling back to primary") w = user32.GetSystemMetrics(0) # SM_CXSCREEN h = user32.GetSystemMetrics(1) # SM_CYSCREEN rects = [(0, 0, w, h)] wndproc = WNDPROC(_wndproc) cls_name = "BlackScreenDimCls" hinst = kernel32.GetModuleHandleW(None) wc = WNDCLASSEXW() wc.cbSize = ctypes.sizeof(WNDCLASSEXW) wc.lpfnWndProc = wndproc wc.hInstance = hinst wc.hbrBackground = gdi32.CreateSolidBrush(0x000000) wc.lpszClassName = cls_name if not user32.RegisterClassExW(ctypes.byref(wc)): LOG.error("RegisterClassExW failed: %d", kernel32.GetLastError()) return for (left, top, right, bottom) in rects: w = right - left h = bottom - top hwnd = user32.CreateWindowExW( WS_EX_TOPMOST | WS_EX_TOOLWINDOW, cls_name, "Black Screen Dim", WS_POPUP | WS_VISIBLE, left, top, w, h, 0, 0, hinst, 0, ) if not hwnd: LOG.error("CreateWindowExW failed for %s: %d", (left, top, right, bottom), kernel32.GetLastError()) continue user32.ShowWindow(hwnd, 5) # SW_SHOW user32.UpdateWindow(hwnd) user32.SetWindowPos(hwnd, HWND_TOPMOST, left, top, w, h, SWP_SHOWWINDOW) user32.SetTimer(hwnd, 1, 1000, None) _hwnds.append(hwnd) LOG.info("created %d overlay window(s)", len(_hwnds)) # hide cursor for _ in range(12): if user32.ShowCursor(False) < 0: break # global ESC hook kb_proc = HOOKPROC(_low_level_keyboard_proc) _hook_id = user32.SetWindowsHookExW(WH_KEYBOARD_LL, kb_proc, hinst, 0) if not _hook_id: LOG.warning("SetWindowsHookExW failed: %d", kernel32.GetLastError()) # keep display awake while running (prevents monitor standby, which would # make ESC's first press get swallowed by the wake and a ~30s DDC hang) sees = kernel32.SetThreadExecutionState( ES_CONTINUOUS | ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED ) LOG.info("SetThreadExecutionState -> %s", sees) # belt-and-suspenders: emit a tiny (invisible) mouse move every 30s so the # OS and any monitor hardware idle timer never trip during long idle. _activity_stop = threading.Event() def _activity(): while not _activity_stop.wait(30): try: user32.mouse_event(0x0001, 1, 0, 0, None) user32.mouse_event(0x0001, 0xFFFFFFFF, 0, 0, None) except Exception: pass at = threading.Thread(target=_activity, daemon=True) at.start() msg = wt.MSG() while user32.GetMessageW(ctypes.byref(msg), 0, 0, 0) > 0: user32.TranslateMessage(ctypes.byref(msg)) user32.DispatchMessageW(ctypes.byref(msg)) if _hook_id: user32.UnhookWindowsHookEx(_hook_id) _activity_stop.set() at.join(timeout=1) kernel32.SetThreadExecutionState(ES_CONTINUOUS) # allow display sleep again for h in _hwnds: user32.DestroyWindow(h) # restore cursor for _ in range(12): if user32.ShowCursor(True) >= 0: break def _dim_and_save(bc): bc.save() bc.dim() def main(): setup_logging() LOG.info("starting Black Screen Dim") # Show the black overlay FIRST so the screen goes black immediately. # Brightness saving/dimming runs in the background, invisible behind it. bc = BrightnessController() dim_thread = threading.Thread(target=_dim_and_save, args=(bc,), daemon=True) dim_thread.start() try: if sys.platform == "win32": run_overlay() else: LOG.warning("non-Windows platform; nothing to display") except Exception as e: LOG.exception("overlay error: %r", e) finally: dim_thread.join(timeout=10) bc.restore() LOG.info("exiting") if __name__ == "__main__": main()