|
#!/usr/bin/env python3 |
|
import subprocess |
|
import sys |
|
import time |
|
from ctypes import CDLL, POINTER, byref, c_bool, c_int, c_uint32, c_void_p, util |
|
|
|
# --- Framework Setup --- |
|
# Use absolute paths for stability on some macOS environments |
|
cg_path = util.find_library("CoreGraphics") |
|
if not cg_path: |
|
cg_path = "/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics" |
|
cg = CDLL(cg_path) |
|
|
|
# CoreGraphics Bindings with explicit error checking |
|
cg.CGBeginDisplayConfiguration.argtypes = [POINTER(c_void_p)] |
|
cg.CGCompleteDisplayConfiguration.argtypes = [c_void_p, c_int] |
|
cg.CGSConfigureDisplayEnabled.argtypes = [c_void_p, c_uint32, c_bool] |
|
cg.CGGetActiveDisplayList.argtypes = [c_uint32, POINTER(c_uint32), POINTER(c_uint32)] |
|
cg.CGGetOnlineDisplayList.argtypes = [c_uint32, POINTER(c_uint32), POINTER(c_uint32)] |
|
cg.CGDisplayIsBuiltin.restype = c_bool |
|
cg.CGDisplayIsBuiltin.argtypes = [c_uint32] |
|
|
|
# For resolution - using simplified calls to avoid mode-copy crashes |
|
cg.CGDisplayPixelsWide.argtypes = [c_uint32] |
|
cg.CGDisplayPixelsWide.restype = c_int |
|
cg.CGDisplayPixelsHigh.argtypes = [c_uint32] |
|
cg.CGDisplayPixelsHigh.restype = c_int |
|
|
|
# Dynamic timeout |
|
timeout = 30 |
|
|
|
|
|
def get_display_basic_info(display_id): |
|
"""Fetches info using safe, direct calls instead of Mode pointers.""" |
|
try: |
|
w = cg.CGDisplayPixelsWide(display_id) |
|
h = cg.CGDisplayPixelsHigh(display_id) |
|
is_builtin = bool(cg.CGDisplayIsBuiltin(display_id)) |
|
name = "Built-in" if is_builtin else "External" |
|
return name, f"{w}x{h}", is_builtin |
|
except Exception: |
|
return "Unknown", "Unknown", False |
|
|
|
|
|
def get_active_ids(): |
|
max_displays = 16 |
|
count = c_uint32() |
|
ids = (c_uint32 * max_displays)() |
|
if cg.CGGetActiveDisplayList(max_displays, ids, byref(count)) == 0: |
|
return [ids[i] for i in range(count.value)] |
|
return [] |
|
|
|
|
|
def get_online_ids(): |
|
max_displays = 16 |
|
count = c_uint32() |
|
ids = (c_uint32 * max_displays)() |
|
if cg.CGGetOnlineDisplayList(max_displays, ids, byref(count)) == 0: |
|
return [ids[i] for i in range(count.value)] |
|
return [] |
|
|
|
|
|
def set_state(display_id, state): |
|
config_ref = c_void_p() |
|
if cg.CGBeginDisplayConfiguration(byref(config_ref)) == 0: |
|
# CGSConfigureDisplayEnabled is a private API, |
|
# return code 0 means success |
|
res = cg.CGSConfigureDisplayEnabled(config_ref, display_id, state) |
|
cg.CGCompleteDisplayConfiguration(config_ref, 0) |
|
return res == 0 |
|
return False |
|
|
|
|
|
def profiler_only_1080p_fhd(): |
|
"""Fallback detector for stale active display IDs after unplug events.""" |
|
try: |
|
output = subprocess.check_output( |
|
["system_profiler", "SPDisplaysDataType"], |
|
text=True, |
|
stderr=subprocess.DEVNULL, |
|
) |
|
except Exception: |
|
return False |
|
|
|
resolution_lines = [] |
|
for line in output.splitlines(): |
|
stripped = line.strip() |
|
if stripped.startswith("Resolution:"): |
|
resolution_lines.append(stripped) |
|
return len(resolution_lines) == 1 and "1080p FHD" in resolution_lines[0] |
|
|
|
|
|
def get_profiler_display_counts(): |
|
"""Returns (internal_count, external_count) parsed from system_profiler.""" |
|
try: |
|
output = subprocess.check_output( |
|
["system_profiler", "SPDisplaysDataType", "-detailLevel", "mini"], |
|
text=True, |
|
stderr=subprocess.DEVNULL, |
|
timeout=8, |
|
) |
|
except Exception: |
|
return 0, 0 |
|
|
|
def flush_record(block_lines, counts): |
|
if not block_lines: |
|
return |
|
block = "\n".join(block_lines).lower() |
|
is_internal = ( |
|
"connection type: internal" in block |
|
or "display type: built-in" in block |
|
or "display type: built in" in block |
|
) |
|
if is_internal: |
|
counts["internal"] += 1 |
|
else: |
|
counts["external"] += 1 |
|
|
|
counts = {"internal": 0, "external": 0} |
|
in_displays = False |
|
displays_indent = -1 |
|
current_block = [] |
|
have_name = False |
|
|
|
for line in output.splitlines(): |
|
indent = len(line) - len(line.lstrip()) |
|
stripped = line.strip() |
|
if not stripped: |
|
continue |
|
|
|
if stripped.lower() == "displays:": |
|
flush_record(current_block, counts) |
|
in_displays = True |
|
displays_indent = indent |
|
current_block = [] |
|
have_name = False |
|
continue |
|
|
|
if not in_displays: |
|
continue |
|
|
|
# End of Displays section. |
|
if indent <= displays_indent: |
|
flush_record(current_block, counts) |
|
in_displays = False |
|
current_block = [] |
|
have_name = False |
|
continue |
|
|
|
# Start of a display record "<name>:" |
|
if line.rstrip().endswith(":") and indent == displays_indent + 2: |
|
flush_record(current_block, counts) |
|
current_block = [] |
|
have_name = True |
|
continue |
|
|
|
if have_name and indent > displays_indent + 2: |
|
current_block.append(stripped) |
|
|
|
flush_record(current_block, counts) |
|
return counts["internal"], counts["external"] |
|
|
|
|
|
# --- CLI Handlers --- |
|
|
|
|
|
def run_list(): |
|
ids = get_active_ids() |
|
if not ids: |
|
print("No active displays found.") |
|
return |
|
print(f"\n{'ID':<15} | {'Type':<12} | {'Resolution':<12}") |
|
print("-" * 45) |
|
for d_id in ids: |
|
name, res, _ = get_display_basic_info(d_id) |
|
print(f"{d_id:<15} | {name:<12} | {res:<12}") |
|
|
|
|
|
def run_auto(): |
|
print("🚀 Auto-Mode Active. (Ctrl+C to stop)") |
|
disabled_set = set() |
|
|
|
try: |
|
while True: |
|
active_ids = get_active_ids() |
|
online_ids = get_online_ids() |
|
print( |
|
f"[{time.strftime('%H:%M:%S')}] [DEBUG] Active IDs: " |
|
f"{', '.join(str(d_id) for d_id in active_ids) if active_ids else '(none)'} | " |
|
f"Online IDs: {', '.join(str(d_id) for d_id in online_ids) if online_ids else '(none)'}" |
|
) |
|
current_builtins = [] |
|
detection_ids = online_ids if online_ids else active_ids |
|
has_external_connected_cg = False |
|
|
|
for d_id in active_ids: |
|
name, res, is_builtin = get_display_basic_info(d_id) |
|
print(f"\t- {d_id:<15} | {name:<12} | {res:<12}") |
|
if is_builtin: |
|
current_builtins.append(d_id) |
|
|
|
for d_id in detection_ids: |
|
_, _, is_builtin = get_display_basic_info(d_id) |
|
if not is_builtin: |
|
has_external_connected_cg = True |
|
break |
|
|
|
profiler_internal_count, profiler_external_count = ( |
|
get_profiler_display_counts() |
|
) |
|
has_external_connected_profiler = profiler_external_count > 0 |
|
has_external_connected = ( |
|
has_external_connected_cg or has_external_connected_profiler |
|
) |
|
print( |
|
f"[{time.strftime('%H:%M:%S')}] [DEBUG] Built-ins: " |
|
f"{', '.join(str(d_id) for d_id in current_builtins) if current_builtins else '(none)'} | " |
|
f"External connected: {has_external_connected} " |
|
f"(cg={has_external_connected_cg}, profiler={has_external_connected_profiler}, " |
|
f"profiler counts i={profiler_internal_count}/e={profiler_external_count}) | " |
|
f"Tracked disabled IDs: {', '.join(str(d_id) for d_id in sorted(disabled_set)) if disabled_set else '(none)'}" |
|
) |
|
|
|
profiler_restore_signal = bool(disabled_set) and profiler_only_1080p_fhd() |
|
restore_needed = bool(disabled_set) and ( |
|
(not has_external_connected) or profiler_restore_signal |
|
) |
|
|
|
# Logic: If external detected, turn off built-ins |
|
if has_external_connected and current_builtins: |
|
for b_id in current_builtins: |
|
timeout = 5 |
|
if set_state(b_id, False): |
|
disabled_set.add(b_id) |
|
print( |
|
f"[{time.strftime('%H:%M:%S')}] External detected -> Disabling Internal ({b_id})" |
|
) |
|
|
|
# Logic: If no external, restore |
|
elif restore_needed: |
|
print( |
|
f"[{time.strftime('%H:%M:%S')}] External removed -> Restoring Internal..." |
|
) |
|
for b_id in list(disabled_set): |
|
set_state(b_id, True) |
|
disabled_set.clear() |
|
timeout = 30 |
|
print( |
|
f"[{time.strftime('%H:%M:%S')}] [DEBUG] Tracked disabled IDs after restore: (none)" |
|
) |
|
|
|
time.sleep(timeout) # Faster response time |
|
except KeyboardInterrupt: |
|
print("\nStopping... Restoring screens.") |
|
for b_id in list(disabled_set): |
|
set_state(b_id, True) |
|
|
|
|
|
if __name__ == "__main__": |
|
if len(sys.argv) < 2: |
|
print("Usage: python3 display.py [list | on <id> | off <id> | auto]") |
|
sys.exit(0) |
|
|
|
cmd = sys.argv[1].lower() |
|
if cmd == "list": |
|
run_list() |
|
elif cmd == "auto": |
|
run_auto() |
|
elif cmd in ["on", "off"] and len(sys.argv) == 3: |
|
target = int(sys.argv[2]) |
|
if set_state(target, cmd == "on"): |
|
print(f"Successfully turned {cmd.upper()} display {target}") |
|
else: |
|
print(f"Failed to turn {cmd.upper()} display {target}") |
|
else: |
|
print("Invalid command.") |