@@ -5,7 +5,7 @@ use core_foundation::{
55 base:: { CFRelease , TCFType , kCFAllocatorDefault} ,
66 date:: CFTimeInterval ,
77 number:: { CFBooleanRef , kCFBooleanTrue} ,
8- runloop:: { CFRunLoop , CFRunLoopSource , kCFRunLoopCommonModes} ,
8+ runloop:: { CFRunLoop , CFRunLoopSource , CFRunLoopSourceRef , kCFRunLoopCommonModes} ,
99 string:: { CFStringCreateWithCString , CFStringRef , kCFStringEncodingUTF8} ,
1010} ;
1111use core_graphics:: {
@@ -664,14 +664,47 @@ fn event_tap_thread(
664664 // callback runs on this thread's CFRunLoop. Box-leak the sender
665665 // so the C side has a stable user_info pointer; reclaim it after
666666 // the run loop exits.
667- let display_user_info = Box :: into_raw ( Box :: new ( display_notify_tx) ) as * mut c_void ;
667+ let display_user_info = Box :: into_raw ( Box :: new ( display_notify_tx. clone ( ) ) ) as * mut c_void ;
668668 unsafe {
669669 CGDisplayRegisterReconfigurationCallback (
670670 display_reconfiguration_callback,
671671 display_user_info,
672672 ) ;
673673 }
674674
675+ // Also subscribe to system-power events so we recover from
676+ // sleep/wake, where the Quartz reconfigure callback may not
677+ // fire (or fires before our run loop is processing again, e.g.
678+ // clamshell-disconnect → lid-open). On wake we send the same
679+ // DisplayReconfigured event the existing handler consumes, so
680+ // bounds get refreshed for free.
681+ let mut power_notifier_object: u32 = 0 ;
682+ let mut power_notification_port: * mut c_void = std:: ptr:: null_mut ( ) ;
683+ let power_ctx = Box :: into_raw ( Box :: new ( PowerCtx {
684+ sender : display_notify_tx,
685+ root_port : 0 ,
686+ } ) ) ;
687+ let power_root_port = unsafe {
688+ let port = IORegisterForSystemPower (
689+ power_ctx as * mut c_void ,
690+ & mut power_notification_port,
691+ power_callback,
692+ & mut power_notifier_object,
693+ ) ;
694+ // Stash the root port for the callback's IOAllowPowerChange
695+ // ack — we couldn't know it at Box-construction time because
696+ // it's the registration's return value.
697+ ( * power_ctx) . root_port = port;
698+ if !power_notification_port. is_null ( ) {
699+ let src_ref = IONotificationPortGetRunLoopSource ( power_notification_port) ;
700+ if !src_ref. is_null ( ) {
701+ let src = CFRunLoopSource :: wrap_under_get_rule ( src_ref) ;
702+ CFRunLoop :: get_current ( ) . add_source ( & src, kCFRunLoopCommonModes) ;
703+ }
704+ }
705+ port
706+ } ;
707+
675708 log:: debug!( "running CFRunLoop..." ) ;
676709 CFRunLoop :: run_current ( ) ;
677710 log:: debug!( "event tap thread exiting!..." ) ;
@@ -683,6 +716,15 @@ fn event_tap_thread(
683716 drop ( Box :: from_raw (
684717 display_user_info as * mut Sender < ProducerEvent > ,
685718 ) ) ;
719+
720+ if power_notifier_object != 0 {
721+ let _ = IODeregisterForSystemPower ( & mut power_notifier_object) ;
722+ }
723+ if !power_notification_port. is_null ( ) {
724+ IONotificationPortDestroy ( power_notification_port) ;
725+ }
726+ let _ = power_root_port;
727+ drop ( Box :: from_raw ( power_ctx) ) ;
686728 }
687729
688730 let _ = exit. send ( ( ) ) ;
@@ -719,6 +761,65 @@ fn is_screen_locked() -> bool {
719761 locked
720762}
721763
764+ /// Refcon for the IOKit system-power callback. Bundles the channel
765+ /// sender (so the callback can post `DisplayReconfigured` on wake)
766+ /// and the `io_connect_t` root port (so the callback can ack
767+ /// sleep-related messages with `IOAllowPowerChange`). Built on the
768+ /// event-tap thread, used only by the callback on the same thread —
769+ /// never crosses thread boundaries, so no Send/Sync needed.
770+ struct PowerCtx {
771+ sender : Sender < ProducerEvent > ,
772+ root_port : u32 ,
773+ }
774+
775+ /// IOKit system-power callback. Fires for every power-management
776+ /// transition (CanSleep, WillSleep, WillPowerOn, HasPoweredOn).
777+ /// We only care about `kIOMessageSystemHasPoweredOn` (post-wake);
778+ /// for the sleep-pending messages we just ack so the kernel doesn't
779+ /// hold the system in its "waiting for clients" state for the full
780+ /// 30-second timeout.
781+ extern "C" fn power_callback (
782+ refcon : * mut c_void ,
783+ _service : u32 ,
784+ msg_type : u32 ,
785+ msg_arg : * mut c_void ,
786+ ) {
787+ const K_IO_MESSAGE_CAN_SYSTEM_SLEEP : u32 = 0xE000_0270 ;
788+ const K_IO_MESSAGE_SYSTEM_WILL_SLEEP : u32 = 0xE000_0280 ;
789+ const K_IO_MESSAGE_SYSTEM_HAS_POWERED_ON : u32 = 0xE000_0300 ;
790+
791+ if refcon. is_null ( ) {
792+ return ;
793+ }
794+ // SAFETY: `refcon` is `Box::into_raw(Box::new(PowerCtx))` owned by
795+ // `event_tap_thread`; valid until the run loop exits and the box
796+ // is reclaimed. The callback only fires while the run loop runs
797+ // on that thread, so the box is live here.
798+ let ctx = unsafe { & * ( refcon as * const PowerCtx ) } ;
799+ match msg_type {
800+ K_IO_MESSAGE_CAN_SYSTEM_SLEEP | K_IO_MESSAGE_SYSTEM_WILL_SLEEP => {
801+ // Ack so the OS doesn't stall on its 30s default timeout.
802+ // `msg_arg` carries the notification ID (an `intptr_t`);
803+ // pass it through verbatim.
804+ unsafe {
805+ IOAllowPowerChange ( ctx. root_port , msg_arg as isize ) ;
806+ }
807+ }
808+ K_IO_MESSAGE_SYSTEM_HAS_POWERED_ON => {
809+ // Bounce a DisplayReconfigured into the producer so
810+ // `update_bounds()` runs. Covers the case where Quartz's
811+ // own reconfigure callback didn't fire (or fired during
812+ // the sleep window) — e.g. clamshell-disconnect →
813+ // lid-open transitions.
814+ log:: info!( "system woke from sleep; refreshing display bounds" ) ;
815+ if let Err ( e) = ctx. sender . blocking_send ( ProducerEvent :: DisplayReconfigured ) {
816+ log:: warn!( "failed to post wake → DisplayReconfigured: {e}" ) ;
817+ }
818+ }
819+ _ => { }
820+ }
821+ }
822+
722823/// Quartz display-reconfiguration callback. Fires twice per change:
723824/// once with `kCGDisplayBeginConfigurationFlag` set (BEFORE the
724825/// change is applied — the bounds are still stale at this point),
@@ -993,6 +1094,31 @@ extern "C" {
9931094 ) -> CGError ;
9941095}
9951096
1097+ #[ link( name = "IOKit" , kind = "framework" ) ]
1098+ extern "C" {
1099+ /// Register the calling process for system-power notifications.
1100+ /// Returns the `io_connect_t` root power port (used later in
1101+ /// `IOAllowPowerChange` to ack sleep-related messages) and writes
1102+ /// the notification port + an `io_object_t` notifier through the
1103+ /// out-pointers. The returned notification port carries a
1104+ /// CFRunLoopSource we attach to this thread's run loop so the
1105+ /// callback fires inline with the existing event-tap loop.
1106+ fn IORegisterForSystemPower (
1107+ refcon : * mut c_void ,
1108+ port_ref : * mut * mut c_void ,
1109+ callback : extern "C" fn ( * mut c_void , u32 , u32 , * mut c_void ) ,
1110+ notifier : * mut u32 ,
1111+ ) -> u32 ;
1112+ fn IODeregisterForSystemPower ( notifier : * mut u32 ) -> i32 ;
1113+ fn IONotificationPortGetRunLoopSource ( notify : * mut c_void ) -> CFRunLoopSourceRef ;
1114+ fn IONotificationPortDestroy ( notify : * mut c_void ) ;
1115+ /// Ack a kIOMessageCanSystemSleep / kIOMessageSystemWillSleep so
1116+ /// the OS doesn't stall on its 30s default timeout waiting for us.
1117+ /// Required even when we have no objection — silence is treated as
1118+ /// "still thinking" by the kernel.
1119+ fn IOAllowPowerChange ( kernel_port : u32 , notification_id : isize ) -> i32 ;
1120+ }
1121+
9961122#[ link( name = "ApplicationServices" , kind = "framework" ) ]
9971123extern "C" {
9981124 fn AXIsProcessTrusted ( ) -> bool ;
0 commit comments