1+ using System . Runtime . InteropServices ;
12using Avalonia ;
23using Avalonia . Controls ;
34using Avalonia . Controls . ApplicationLifetimes ;
@@ -26,7 +27,7 @@ public class App : Application
2627 // Business logic orchestrator
2728 public static ModeControl ? Mode { get ; private set ; }
2829
29- public static MainWindow ? MainWindowInstance { get ; private set ; }
30+ public static MainWindow ? MainWindowInstance { get ; set ; }
3031 public static TrayIcon ? TrayIconInstance { get ; set ; }
3132
3233 // Legacy event codes for non-configurable keys
@@ -112,9 +113,23 @@ public override void OnFrameworkInitializationCompleted()
112113 // Update tray icon to match current mode
113114 UpdateTrayIcon ( ) ;
114115
116+ // Start power state monitoring for auto GPU mode and auto performance
117+ Power ? . StartPowerMonitoring ( ) ;
118+ if ( Power != null )
119+ {
120+ Power . PowerStateChanged += OnPowerStateChanged ;
121+ }
122+
123+ // Apply auto GPU mode on startup if enabled
124+ AutoGpuMode ( ) ;
125+
115126 // Restore clamshell mode if it was enabled
116127 if ( AppConfig . Is ( "toggle_clamshell_mode" ) )
117128 UI . Views . ExtraWindow . StartClamshellInhibit ( ) ;
129+
130+ // Register Unix signal handlers for clean shutdown on SIGTERM/SIGINT
131+ // This prevents KDE/GNOME from hanging on logout/reboot
132+ RegisterSignalHandlers ( desktop ) ;
118133 }
119134
120135 base . OnFrameworkInitializationCompleted ( ) ;
@@ -139,6 +154,9 @@ private void InitializePlatform()
139154 Logger . WriteLine ( $ "Model: { System . GetModelName ( ) } ") ;
140155 Logger . WriteLine ( $ "BIOS: { System . GetBiosVersion ( ) } ") ;
141156
157+ // Log which sysfs backend each attribute resolved to (legacy vs firmware-attributes)
158+ SysfsHelper . LogResolvedAttributes ( ) ;
159+
142160 // Log detected features
143161 LogFeatureDetection ( ) ;
144162 }
@@ -511,14 +529,35 @@ private NativeMenu CreateTrayMenu(IClassicDesktopStyleApplicationLifetime deskto
511529 menu . Add ( new NativeMenuItemSeparator ( ) ) ;
512530
513531 // GPU modes
514- var eco = new NativeMenuItem ( "Eco (iGPU only)" ) ;
515- eco . Click += ( _ , _ ) => SetGpuMode ( ecoEnabled : true ) ;
532+ var eco = new NativeMenuItem ( "GPU: Eco (iGPU only)" ) ;
533+ eco . Click += ( _ , _ ) =>
534+ {
535+ AppConfig . Set ( "gpu_auto" , 0 ) ;
536+ SetGpuMode ( ecoEnabled : true ) ;
537+ MainWindowInstance ? . RefreshGpuModePublic ( ) ;
538+ } ;
516539 menu . Add ( eco ) ;
517540
518- var standard = new NativeMenuItem ( "Standard (dGPU)" ) ;
519- standard . Click += ( _ , _ ) => SetGpuMode ( ecoEnabled : false ) ;
541+ var standard = new NativeMenuItem ( "GPU: Standard (dGPU)" ) ;
542+ standard . Click += ( _ , _ ) =>
543+ {
544+ AppConfig . Set ( "gpu_auto" , 0 ) ;
545+ SetGpuMode ( ecoEnabled : false ) ;
546+ MainWindowInstance ? . RefreshGpuModePublic ( ) ;
547+ } ;
520548 menu . Add ( standard ) ;
521549
550+ var optimized = new NativeMenuItem ( "GPU: Optimized (auto)" ) ;
551+ optimized . Click += ( _ , _ ) =>
552+ {
553+ AppConfig . Set ( "gpu_auto" , 1 ) ;
554+ AutoGpuMode ( ) ;
555+ MainWindowInstance ? . RefreshGpuModePublic ( ) ;
556+ System ? . ShowNotification ( "GPU Mode" ,
557+ "Optimized — auto Eco/Standard based on power" , "video-display" ) ;
558+ } ;
559+ menu . Add ( optimized ) ;
560+
522561 menu . Add ( new NativeMenuItemSeparator ( ) ) ;
523562
524563 // Settings
@@ -541,7 +580,16 @@ private NativeMenu CreateTrayMenu(IClassicDesktopStyleApplicationLifetime deskto
541580
542581 private void ToggleMainWindow ( )
543582 {
544- if ( MainWindowInstance == null ) return ;
583+ // Window may have been disposed by closing (KDE logout, user clicking X).
584+ // Recreate it if needed — app stays alive via ShutdownMode.OnExplicitShutdown.
585+ if ( MainWindowInstance == null || MainWindowInstance . PlatformImpl == null )
586+ {
587+ MainWindowInstance = new MainWindow ( ) ;
588+ if ( AppConfig . Is ( "topmost" ) ) MainWindowInstance . Topmost = true ;
589+ MainWindowInstance . Show ( ) ;
590+ MainWindowInstance . Activate ( ) ;
591+ return ;
592+ }
545593
546594 if ( MainWindowInstance . IsVisible )
547595 {
@@ -562,11 +610,144 @@ private void SetGpuMode(bool ecoEnabled)
562610 System ? . ShowNotification ( "GPU Mode" , status , "video-display" ) ;
563611 }
564612
613+ /// <summary>
614+ /// Handle power state change (AC plugged/unplugged).
615+ /// Triggers auto GPU mode switch and auto performance mode.
616+ /// </summary>
617+ private void OnPowerStateChanged ( bool onAc )
618+ {
619+ Logger . WriteLine ( $ "Power state changed: AC={ onAc } ") ;
620+
621+ // Auto GPU mode (Optimized = auto Eco/Standard based on AC power)
622+ AutoGpuMode ( ) ;
623+
624+ // Auto performance mode (if configured)
625+ Mode ? . AutoPerformance ( powerChanged : true ) ;
626+
627+ // Refresh UI
628+ Avalonia . Threading . Dispatcher . UIThread . Post ( ( ) =>
629+ {
630+ MainWindowInstance ? . RefreshGpuModePublic ( ) ;
631+ } ) ;
632+ }
633+
634+ /// <summary>
635+ /// Auto-switch GPU between Eco and Standard based on AC power state.
636+ /// This implements Windows G-Helper's "Optimized" GPU mode (gpu_auto flag).
637+ /// Called on startup and on every power state change.
638+ /// </summary>
639+ public static void AutoGpuMode ( )
640+ {
641+ if ( ! AppConfig . Is ( "gpu_auto" ) ) return ;
642+
643+ var wmi = Wmi ;
644+ var power = Power ;
645+ if ( wmi == null || power == null ) return ;
646+
647+ // Don't auto-switch if in Ultimate (MUX=0) — user must manually switch out
648+ int mux = wmi . GetGpuMuxMode ( ) ;
649+ if ( mux == 0 )
650+ {
651+ Logger . WriteLine ( "AutoGpuMode: MUX=0 (Ultimate), skipping auto-switch" ) ;
652+ return ;
653+ }
654+
655+ bool onAc = power . IsOnAcPower ( ) ;
656+ bool ecoEnabled = wmi . GetGpuEco ( ) ;
657+
658+ if ( onAc && ecoEnabled )
659+ {
660+ // Plugged in → switch to Standard (enable dGPU)
661+ Logger . WriteLine ( "AutoGpuMode: AC power detected, switching Eco → Standard" ) ;
662+ Task . Run ( ( ) =>
663+ {
664+ try
665+ {
666+ wmi . SetGpuEco ( false ) ;
667+ System ? . ShowNotification ( "GPU Mode" ,
668+ "Optimized: AC power — dGPU enabled" , "video-display" ) ;
669+ }
670+ catch ( Exception ex )
671+ {
672+ Logger . WriteLine ( $ "AutoGpuMode Eco→Standard failed: { ex . Message } ") ;
673+ }
674+ } ) ;
675+ }
676+ else if ( ! onAc && ! ecoEnabled )
677+ {
678+ // On battery → switch to Eco (disable dGPU for battery life)
679+ Logger . WriteLine ( "AutoGpuMode: Battery detected, switching Standard → Eco" ) ;
680+ Task . Run ( ( ) =>
681+ {
682+ try
683+ {
684+ wmi . SetGpuEco ( true ) ;
685+ System ? . ShowNotification ( "GPU Mode" ,
686+ "Optimized: Battery — dGPU disabled" , "video-display" ) ;
687+ }
688+ catch ( Exception ex )
689+ {
690+ Logger . WriteLine ( $ "AutoGpuMode Standard→Eco failed: { ex . Message } ") ;
691+ }
692+ } ) ;
693+ }
694+ }
695+
696+ // Unix signal handlers for clean shutdown on SIGTERM/SIGINT (logout/reboot)
697+ private static List < PosixSignalRegistration > ? _signalRegistrations ;
698+
699+ private void RegisterSignalHandlers ( IClassicDesktopStyleApplicationLifetime desktop )
700+ {
701+ if ( ! RuntimeInformation . IsOSPlatform ( OSPlatform . Linux ) )
702+ return ;
703+
704+ try
705+ {
706+ _signalRegistrations = new ( ) ;
707+
708+ // SIGTERM: sent by KDE/GNOME during logout/reboot
709+ _signalRegistrations . Add ( PosixSignalRegistration . Create ( PosixSignal . SIGTERM , _ =>
710+ {
711+ Logger . WriteLine ( "Received SIGTERM - initiating shutdown" ) ;
712+ ShutdownFromSignal ( desktop ) ;
713+ } ) ) ;
714+
715+ // SIGINT: Ctrl+C in terminal
716+ _signalRegistrations . Add ( PosixSignalRegistration . Create ( PosixSignal . SIGINT , _ =>
717+ {
718+ Logger . WriteLine ( "Received SIGINT - initiating shutdown" ) ;
719+ ShutdownFromSignal ( desktop ) ;
720+ } ) ) ;
721+
722+ Logger . WriteLine ( "Unix signal handlers registered (SIGTERM, SIGINT)" ) ;
723+ }
724+ catch ( Exception ex )
725+ {
726+ Logger . WriteLine ( $ "Failed to register signal handlers: { ex . Message } ") ;
727+ }
728+ }
729+
730+ private void ShutdownFromSignal ( IClassicDesktopStyleApplicationLifetime desktop )
731+ {
732+ // Signal handler runs on a threadpool thread.
733+ // Don't rely on UI thread — it may already be blocked during session shutdown.
734+ Logger . WriteLine ( "Signal shutdown: cleaning up..." ) ;
735+
736+ try { Power ? . StopPowerMonitoring ( ) ; } catch { }
737+ try { UI . Views . ExtraWindow . StopClamshellInhibit ( ) ; } catch { }
738+ try { Input ? . Dispose ( ) ; } catch { }
739+ try { Wmi ? . Dispose ( ) ; } catch { }
740+
741+ Logger . WriteLine ( "Signal shutdown: exiting process" ) ;
742+ Environment . Exit ( 0 ) ;
743+ }
744+
565745 private void Shutdown ( IClassicDesktopStyleApplicationLifetime desktop )
566746 {
567747 Logger . WriteLine ( "Shutting down..." ) ;
568748
569749 // Cleanup
750+ Power ? . StopPowerMonitoring ( ) ;
570751 UI . Views . ExtraWindow . StopClamshellInhibit ( ) ;
571752 Input ? . Dispose ( ) ;
572753 Wmi ? . Dispose ( ) ;
0 commit comments