From 236d6597c35d9de77252321a47ec88d9102c720a Mon Sep 17 00:00:00 2001 From: Spencer Brooks Date: Tue, 6 Jan 2026 12:53:32 -0800 Subject: [PATCH 1/2] added mesa-utils as dependency This is a really small change to the dependencies. --- .github/workflows/build-packages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-packages.yml b/.github/workflows/build-packages.yml index 1b7a3ac..8d6deda 100644 --- a/.github/workflows/build-packages.yml +++ b/.github/workflows/build-packages.yml @@ -89,7 +89,7 @@ jobs: Section: utils Priority: optional Architecture: all - Depends: python3, python3-gi, python3-dbus, gir1.2-gtk-3.0, gir1.2-appindicator3-0.1, gir1.2-notify-0.7, gir1.2-upowerglib-1.0, polkitd, udev + Depends: python3, python3-gi, python3-dbus, gir1.2-gtk-3.0, gir1.2-appindicator3-0.1, gir1.2-notify-0.7, gir1.2-upowerglib-1.0, polkitd, udev, mesa-utils Maintainer: Matt Hartley Description: Manual GPU switching and automatic power profile management System tray application for manual GPU mode switching on NVIDIA Optimus From 3837e94bdc1c3d8e462c1c745adb3a25444683ba Mon Sep 17 00:00:00 2001 From: Spencer Brooks Date: Wed, 7 Jan 2026 10:14:14 -0800 Subject: [PATCH 2/2] Improve UI and state tracking for pending GPU mode changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # GPUMode.py Changes Analysis This is a summary of the changes made agains HEAD: `6b0523e`. In general, I wanted to clean up the UI to make it more intuitve. As part of that there were some state management changes that had to be made. --- ## Summary **Version bump:** 1.01 → 1.02 **Overall scope:** Major refactor of state management to properly track pending GPU mode changes requiring reboot, plus UI improvements with power indicators and checkbox-style menu items. --- ## Changes Made ### 1. Added State Persistence System **Files/Lines:** Lines 14, 21-22, 65-68, 125-192 **What changed:** - Added `json` import - Added `STATE_FILE = LOG_DIR / "state.json"` constant - Replaced single `self.current_mode` with dual tracking: - `self.boot_mode` - actual GPU mode detected at startup (never changes until reboot) - `self.target_mode` - pending mode if different from boot_mode **New methods:** - `get_system_boot_time()` - reads btime from `/proc/stat` - `load_state()` - loads persisted target_mode, clears if reboot detected - `save_state()` - persists target_mode + boot_time to JSON - `has_pending_change()` - returns True if target_mode differs from boot_mode - `set_target_mode()` - sets target and saves state **Why:** Previously, the UI would show the *configured* mode as active immediately after switching, even though the actual GPU doesn't change until reboot. This confused users into thinking the switch had taken effect. **⚠️ POTENTIAL ISSUE:** JSON state file could become corrupt if write is interrupted. No error recovery beyond logging. Consider atomic write (write to temp, rename). --- ### 2. Added Power Consumption Indicators **Files/Lines:** Lines 24-29 **What changed:** ```python POWER_INDICATORS = { 'integrated': '⚡', 'hybrid': '⚡⚡', 'nvidia': '⚡⚡⚡' } ``` Menu labels now include these indicators (e.g., "Integrated ⚡"). **Why:** Visual hint about relative power consumption of each mode. --- ### 3. Added Mode Icons Dictionary **Files/Lines:** Lines 31-36 **What changed:** ```python MODE_ICONS = { 'integrated': 'drive-harddisk-solidstate-symbolic', 'hybrid': 'video-single-display-symbolic', 'nvidia': 'video-display-symbolic' } ``` Replaces inline icon selection in `update_icon()`. **Why:** Centralizes icon mapping, easier to maintain. --- ### 4. Changed Menu Items to CheckMenuItems **Files/Lines:** Lines 462-482 (build_menu) **What changed:** - GPU mode options changed from `Gtk.MenuItem` to `Gtk.CheckMenuItem` - Uses `set_active()` to show checkmark on current/pending mode - Removed `switch_integrated()` and `switch_hybrid()` wrapper methods - Added `on_integrated_toggled()` and `on_hybrid_toggled()` handlers **Why:** Checkbox UI is more intuitive for mutually exclusive mode selection. **⚠️ POTENTIAL ISSUE:** `CheckMenuItem` `toggled` signal fires both when checking AND unchecking. The handlers must guard against recursive/unwanted toggles. Current code does handle this but it's fragile: ```python def on_integrated_toggled(self, widget): if widget.get_active() and selected_mode != 'integrated': self.switch_gpu('integrated') elif not widget.get_active() and selected_mode == 'integrated': widget.set_active(True) # Re-check if user tried to uncheck active ``` --- ### 5. Added "(ON REBOOT)" Label Suffix **Files/Lines:** Lines 463-466, 473-476 **What changed:** ```python if selected_mode == 'integrated' and self.boot_mode != 'integrated': integrated_label += ' (ON REBOOT)' ``` **Why:** Makes it clear that the selected mode won't take effect until reboot. --- ### 6. Changed Menu Header Format **Files/Lines:** Line 425 **What changed:** - Old: `━━━ Current: {mode.upper()} ━━━` - New: `ACTIVE MODE: {mode.upper()}` **Why:** Cleaner, less decorative. --- ### 7. Changed RTD3 Parameter Value **Files/Lines:** Lines 298, 558 **What changed:** - Old: `cmd.extend(['--rtd3', '2'])` - New: `cmd.extend(['--rtd3', '3'])` **Why:** RTD3 level 3 is more aggressive power saving for the NVIDIA GPU in hybrid mode. Framework laptops with discrete GPUs are modern hardware that fully supports RTD3 level 3, so this is the appropriate setting for better battery life in hybrid mode. --- ### 8. Changed Reboot Command to Use pkexec **Files/Lines:** Lines 310-315 **What changed:** - Old: `subprocess.run(['systemctl', 'reboot'], timeout=5)` - New: `subprocess.run(['pkexec', 'systemctl', 'reboot'], timeout=60)` Also added handling for reboot cancellation: ```python if reboot_result.returncode != 0: logging.info("Reboot cancelled or failed, showing pending state") GLib.idle_add(self.switch_complete, mode, True, None) ``` **Why:** `systemctl reboot` requires root. Using pkexec prompts for authentication. If user cancels, UI now correctly shows pending state instead of hanging. **⚠️ POTENTIAL ISSUE:** User now gets TWO pkexec prompts when using switch_and_reboot: one for envycontrol, one for reboot. This is slightly annoying UX but necessary for security. --- ### 9. Updated switch_complete() Logic **Files/Lines:** Lines 600-638 **What changed:** - Now calls `self.set_target_mode(mode)` to persist state - Different notification based on `has_pending_change()`: - Pending change: "Reboot to finish the switch to MODE mode." - No pending (restored to boot mode): "Restored to MODE mode. No reboot required." **Why:** Better user feedback. If user switches back to boot mode, no reboot is needed. --- ### 10. Gutted refresh_mode() Method **Files/Lines:** Lines 407-412 **What changed:** ```python def refresh_mode(self): """Refresh menu state (boot_mode is fixed at startup...)""" # boot_mode is intentionally NOT refreshed here... pass ``` Also removed from menu's 'show' signal connection. **Why:** boot_mode should NEVER change during runtime - it represents actual GPU state at boot. Refreshing it would defeat the purpose of the new state tracking. **⚠️ POTENTIAL ISSUE:** Method is now a no-op but still exists. Could confuse future maintainers. Consider removing entirely or renaming to clarify intent. --- ### 11. Changed CheckMenuItem Signal for Power Prompts **Files/Lines:** Line 499 **What changed:** - Old: `power_prompts_item.connect('activate', self.toggle_power_prompts)` - New: `power_prompts_item.connect('toggled', self.toggle_power_prompts)` **Why:** `toggled` is the correct signal for CheckMenuItem state changes. --- ### 12. Menu Structure Changes (NVIDIA mode) **Files/Lines:** Lines 432-456 **What changed:** - Removed "⚠ NVIDIA Mode Active" warning line (redundant with header) - Moved "Set BIOS to Hybrid (F2)..." message to after Integrated/Hybrid options - NVIDIA CheckMenuItem kept sensitive (white text) but no handler connected - Uses CheckMenuItem with `set_active(True)` for NVIDIA when in NVIDIA mode **Why:** Less redundant, cleaner visual hierarchy. --- ### 13. Removed Separator Before About **Files/Lines:** Line 504 **What changed:** - Old: Had `Gtk.SeparatorMenuItem()` before Quit only - New: Has `Gtk.SeparatorMenuItem()` before Power Prompts and before About **Why:** Better visual grouping. --- ## Potential Issues Summary | Issue | Severity | Description | |-------|----------|-------------| | JSON state corruption | Low | No atomic write for state.json | | Double pkexec prompt | Low | UX annoyance in switch_and_reboot flow | | Empty refresh_mode() | Low | Dead code, could confuse maintainers | | CheckMenuItem toggle guards | Low | Fragile re-check logic if user unchecks active mode | --- ## Files Changed - `GPUMode.py` - All changes above ## New Dependencies - None (json is stdlib) ## New Files Created at Runtime - `~/.local/share/gpumode/state.json` - Persists target_mode and boot_time --- GPUMode.py | 355 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 238 insertions(+), 117 deletions(-) diff --git a/GPUMode.py b/GPUMode.py index 77ea7d2..a8e7f62 100644 --- a/GPUMode.py +++ b/GPUMode.py @@ -11,13 +11,30 @@ import sys import fcntl import logging +import json from pathlib import Path -VERSION = "1.01" +VERSION = "1.02" LOCK_FILE = "/tmp/gpumode.lock" LOG_DIR = Path.home() / ".local/share/gpumode" LOG_FILE = LOG_DIR / "gpumode.log" SETTINGS_FILE = LOG_DIR / "settings.conf" +STATE_FILE = LOG_DIR / "state.json" + +# Power consumption indicators +POWER_INDICATORS = { + 'integrated': '⚡', + 'hybrid': '⚡⚡', + 'nvidia': '⚡⚡⚡' +} + +# Icon names for each mode (used in tray) +MODE_ICONS = { + 'integrated': 'drive-harddisk-solidstate-symbolic', + 'hybrid': 'video-single-display-symbolic', + 'nvidia': 'video-display-symbolic' +} + class GPUIndicator: def __init__(self): @@ -44,18 +61,24 @@ def __init__(self): self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE) self.switching = False - self.current_mode = self.get_current_mode() + + # State tracking for reboot indicator + self.boot_mode = self.get_current_mode() # Actual GPU from glxinfo + self.target_mode = None # Pending mode (differs from boot_mode if reboot needed) + self.load_state() # Load target_mode and check for reboot + self.update_icon() - + self.power_prompts_enabled = self.load_power_prompts_setting() - + self.indicator.set_menu(self.build_menu()) - + self.upower_client = UPowerGlib.Client.new() self.upower_client.connect('notify::on-battery', self.on_power_changed) self.last_power_state = self.upower_client.get_on_battery() - - logging.info(f"Initial GPU mode: {self.current_mode}") + + logging.info(f"Boot GPU mode: {self.boot_mode}") + logging.info(f"Target GPU mode: {self.target_mode}") logging.info(f"Initial power state: {'battery' if self.last_power_state else 'AC'}") logging.info(f"Power prompts enabled: {self.power_prompts_enabled}") @@ -66,18 +89,18 @@ def check_startup_mismatch(self): if not self.power_prompts_enabled: logging.info("Power prompts disabled, skipping startup check") return False - + on_battery = self.upower_client.get_on_battery() - - if on_battery and self.current_mode in ['nvidia', 'hybrid']: + + if on_battery and self.boot_mode in ['nvidia', 'hybrid']: logging.info("Startup mismatch: On battery but using NVIDIA/Hybrid") self.prompt_switch_on_battery() - elif not on_battery and self.current_mode == 'integrated': + elif not on_battery and self.boot_mode == 'integrated': logging.info("Startup mismatch: On AC but using Integrated") self.prompt_switch_on_ac() else: logging.info("No startup mismatch detected") - + return False def load_power_prompts_setting(self): @@ -99,6 +122,75 @@ def save_power_prompts_setting(self, enabled): except Exception as e: logging.error(f"Failed to save power prompts setting: {e}") + def get_system_boot_time(self): + """Get system boot time from /proc/stat""" + try: + with open('/proc/stat', 'r') as f: + for line in f: + if line.startswith('btime'): + return int(line.split()[1]) + except Exception as e: + logging.error(f"Failed to get boot time: {e}") + return None + + def load_state(self): + """Load target_mode from state file, clearing if reboot occurred""" + if not STATE_FILE.exists(): + self.target_mode = None + return + + try: + state = json.loads(STATE_FILE.read_text()) + saved_boot_time = state.get('boot_time') + saved_target_mode = state.get('target_mode') + current_boot_time = self.get_system_boot_time() + + # If boot time changed, system rebooted - clear pending state + if saved_boot_time != current_boot_time: + logging.info("Reboot detected, clearing pending state") + self.target_mode = None + self.save_state() + return + + # If target_mode matches boot_mode, no pending change + if saved_target_mode == self.boot_mode: + logging.info("Target mode matches boot mode, no pending change") + self.target_mode = None + self.save_state() + return + + self.target_mode = saved_target_mode + logging.info(f"Loaded pending target mode: {self.target_mode}") + + except Exception as e: + logging.error(f"Failed to load state: {e}") + self.target_mode = None + + def save_state(self): + """Save target_mode and boot time to state file""" + try: + state = { + 'boot_time': self.get_system_boot_time(), + 'target_mode': self.target_mode + } + STATE_FILE.write_text(json.dumps(state)) + logging.info(f"Saved state: target_mode={self.target_mode}") + except Exception as e: + logging.error(f"Failed to save state: {e}") + + def has_pending_change(self): + """Check if there's a pending GPU mode change requiring reboot""" + return self.target_mode is not None and self.target_mode != self.boot_mode + + def set_target_mode(self, mode): + """Set target mode and save state""" + if mode == self.boot_mode: + # Switching back to boot mode - no reboot needed + self.target_mode = None + else: + self.target_mode = mode + self.save_state() + def toggle_power_prompts(self, widget): """Toggle power change prompts on/off""" self.power_prompts_enabled = widget.get_active() @@ -132,7 +224,7 @@ def on_power_changed(self, client, pspec): def prompt_switch_on_battery(self): """Prompt to switch to integrated when on battery""" - if self.current_mode == "integrated": + if self.boot_mode == "integrated": logging.info("Already on integrated, skipping battery prompt") return @@ -160,7 +252,7 @@ def prompt_switch_on_battery(self): def prompt_switch_on_ac(self): """Prompt to switch to Hybrid when on AC""" - if self.current_mode == "hybrid": + if self.boot_mode == "hybrid": logging.info("Already on Hybrid, skipping AC prompt") return @@ -191,38 +283,44 @@ def switch_and_reboot(self, mode): self.switching = True self.update_icon() self.indicator.set_menu(self.build_menu()) - + notification = Notify.Notification.new( "GPUMode", f"Switching to {mode} mode and rebooting...", "emblem-synchronizing" ) notification.show() - + def switch_reboot_thread(): try: cmd = ['pkexec', 'envycontrol', '-s', mode] if mode == 'hybrid': - cmd.extend(['--rtd3', '2']) - + cmd.extend(['--rtd3', '3']) + result = subprocess.run( cmd, - capture_output=True, - text=True, + capture_output=True, + text=True, timeout=60 ) - + if result.returncode == 0: logging.info(f"Switched to {mode}, initiating reboot") - subprocess.run(['systemctl', 'reboot'], timeout=5) + GLib.idle_add(self.set_target_mode, mode) + # Reboot requires privileges - use pkexec + reboot_result = subprocess.run(['pkexec', 'systemctl', 'reboot'], timeout=60) + if reboot_result.returncode != 0: + # Reboot cancelled/failed - update UI to show pending state + logging.info("Reboot cancelled or failed, showing pending state") + GLib.idle_add(self.switch_complete, mode, True, None) else: logging.error(f"Failed to switch: {result.stderr}") GLib.idle_add(self.switch_complete, mode, False, result.stderr) - + except Exception as e: logging.error(f"Switch and reboot error: {e}") GLib.idle_add(self.switch_complete, mode, False, str(e)) - + thread = threading.Thread(target=switch_reboot_thread) thread.daemon = True thread.start() @@ -264,9 +362,10 @@ def get_current_mode(self): logging.info("Detected NVIDIA-only mode via glxinfo (BIOS-set)") return "nvidia" elif 'AMD' in renderer and 'NVIDIA' not in renderer: - # AMD only - check envycontrol to confirm integrated mode + # AMD only in glxinfo - query envycontrol for actual mode + # (hybrid mode uses AMD by default, NVIDIA on-demand) try: - result = subprocess.run(['envycontrol', '--query'], + result = subprocess.run(['envycontrol', '--query'], capture_output=True, text=True, timeout=2) if result.returncode == 0: mode = result.stdout.strip().lower() @@ -274,7 +373,7 @@ def get_current_mode(self): return mode except: pass - logging.info("Detected integrated mode via glxinfo") + logging.info("Detected integrated mode via glxinfo (AMD only)") return "integrated" elif 'AMD' in renderer and 'NVIDIA' in renderer: # Both GPUs in renderer string - likely hybrid @@ -300,126 +399,140 @@ def update_icon(self): """Update tray icon based on current state""" if self.switching: self.indicator.set_icon("emblem-synchronizing-symbolic") - elif self.current_mode == "integrated": - self.indicator.set_icon("drive-harddisk-solidstate-symbolic") - elif self.current_mode == "nvidia": - self.indicator.set_icon("video-display-symbolic") - elif self.current_mode == "hybrid": - self.indicator.set_icon("video-single-display-symbolic") + elif self.boot_mode in MODE_ICONS: + self.indicator.set_icon(MODE_ICONS[self.boot_mode]) else: self.indicator.set_icon("dialog-question-symbolic") def refresh_mode(self): - """Refresh current GPU mode""" - if not self.switching: - old_mode = self.current_mode - self.current_mode = self.get_current_mode() - if old_mode != self.current_mode: - logging.info(f"GPU mode changed: {old_mode} -> {self.current_mode}") - self.update_icon() - self.indicator.set_menu(self.build_menu()) + """Refresh menu state (boot_mode is fixed at startup, only changes after reboot)""" + # boot_mode is intentionally NOT refreshed here - it represents the actual + # GPU mode at boot time. envycontrol config may change before reboot, + # but the actual GPU doesn't change until reboot. + pass def build_menu(self): """Build the indicator menu""" + # Reload setting fresh each time menu is built + self.power_prompts_enabled = self.load_power_prompts_setting() + menu = Gtk.Menu() - - menu.connect('show', lambda _: self.refresh_mode()) - + + # Status line if self.switching: status = Gtk.MenuItem(label='━━━ SWITCHING... ━━━') else: - status = Gtk.MenuItem(label=f'━━━ Current: {self.current_mode.upper()} ━━━') + status = Gtk.MenuItem(label=f'ACTIVE MODE: {self.boot_mode.upper()}') status.set_sensitive(False) menu.append(status) - + menu.append(Gtk.SeparatorMenuItem()) - - # If in NVIDIA mode, show blocking message - if self.current_mode == 'nvidia': - blocked_warning = Gtk.MenuItem(label='⚠ NVIDIA Mode Active') - blocked_warning.set_sensitive(False) - menu.append(blocked_warning) - - blocked_msg = Gtk.MenuItem(label='Set BIOS to Hybrid (F2) to enable switching') - blocked_msg.set_sensitive(False) - menu.append(blocked_msg) - - menu.append(Gtk.SeparatorMenuItem()) - - # Show all modes as disabled - integrated = Gtk.MenuItem(label='⚪ Integrated GPU') + + # If in NVIDIA mode (BIOS-set), show switchable modes as disabled + if self.boot_mode == 'nvidia': + # Integrated - disabled in NVIDIA mode + integrated = Gtk.CheckMenuItem(label=f'Integrated {POWER_INDICATORS["integrated"]}') + integrated.set_active(False) integrated.set_sensitive(False) menu.append(integrated) - - hybrid = Gtk.MenuItem(label='⚪ Hybrid Mode') + + # Hybrid - disabled in NVIDIA mode + hybrid = Gtk.CheckMenuItem(label=f'Hybrid {POWER_INDICATORS["hybrid"]}') + hybrid.set_active(False) hybrid.set_sensitive(False) menu.append(hybrid) - - nvidia = Gtk.MenuItem(label='● NVIDIA GPU (ACTIVE)') - nvidia.set_sensitive(False) + + # Info message at bottom of switchable section + bios_msg = Gtk.MenuItem(label=' Set BIOS to Hybrid (F2) to enable switching') + bios_msg.set_sensitive(False) + menu.append(bios_msg) + + menu.append(Gtk.SeparatorMenuItem()) + + # NVIDIA - active and white text (sensitive=True but not clickable) + nvidia = Gtk.CheckMenuItem(label=f'NVIDIA {POWER_INDICATORS["nvidia"]}') + nvidia.set_active(True) + # Keep sensitive for white text, but don't connect a handler menu.append(nvidia) else: - # Normal mode - Warning about NVIDIA mode - warning = Gtk.MenuItem(label='⚠ NVIDIA mode: Use BIOS (F2)') - warning.set_sensitive(False) - menu.append(warning) - - menu.append(Gtk.SeparatorMenuItem()) - - # Integrated - switchable - integrated = Gtk.MenuItem( - label='⚪ Integrated GPU' if self.current_mode != 'integrated' - else '● Integrated GPU (ACTIVE)' - ) - integrated.connect('activate', self.switch_integrated) - if self.current_mode == 'integrated' or self.switching: + # Normal mode - switchable between integrated and hybrid + # User's selection is target_mode if set, otherwise boot_mode + selected_mode = self.target_mode if self.target_mode else self.boot_mode + + # Integrated + integrated_label = f'Integrated {POWER_INDICATORS["integrated"]}' + if selected_mode == 'integrated' and self.boot_mode != 'integrated': + integrated_label += ' (ON REBOOT)' + integrated = Gtk.CheckMenuItem(label=integrated_label) + integrated.set_active(selected_mode == 'integrated') + integrated.connect('toggled', self.on_integrated_toggled) + if self.switching: integrated.set_sensitive(False) menu.append(integrated) - - # Hybrid - switchable - hybrid = Gtk.MenuItem( - label='⚪ Hybrid Mode' if self.current_mode != 'hybrid' - else '● Hybrid Mode (ACTIVE)' - ) - hybrid.connect('activate', self.switch_hybrid) - if self.current_mode == 'hybrid' or self.switching: + + # Hybrid + hybrid_label = f'Hybrid {POWER_INDICATORS["hybrid"]}' + if selected_mode == 'hybrid' and self.boot_mode != 'hybrid': + hybrid_label += ' (ON REBOOT)' + hybrid = Gtk.CheckMenuItem(label=hybrid_label) + hybrid.set_active(selected_mode == 'hybrid') + hybrid.connect('toggled', self.on_hybrid_toggled) + if self.switching: hybrid.set_sensitive(False) menu.append(hybrid) - - # NVIDIA - show status only, not switchable - nvidia = Gtk.MenuItem(label='⚪ NVIDIA GPU') + + menu.append(Gtk.SeparatorMenuItem()) + + # NVIDIA - info only, not switchable from software + nvidia = Gtk.MenuItem(label=f'NVIDIA {POWER_INDICATORS["nvidia"]}') nvidia.set_sensitive(False) menu.append(nvidia) - + + nvidia_info = Gtk.MenuItem(label=' Set in BIOS (F2)') + nvidia_info.set_sensitive(False) + menu.append(nvidia_info) + menu.append(Gtk.SeparatorMenuItem()) - + power_prompts_item = Gtk.CheckMenuItem(label='Prompt on Power Change') power_prompts_item.set_active(self.power_prompts_enabled) - power_prompts_item.connect('activate', self.toggle_power_prompts) - if self.switching or self.current_mode == 'nvidia': + power_prompts_item.connect('toggled', self.toggle_power_prompts) + if self.switching or self.boot_mode == 'nvidia': power_prompts_item.set_sensitive(False) menu.append(power_prompts_item) - + + menu.append(Gtk.SeparatorMenuItem()) + about_item = Gtk.MenuItem(label='About') about_item.connect('activate', self.show_about) menu.append(about_item) - - menu.append(Gtk.SeparatorMenuItem()) - + quit_item = Gtk.MenuItem(label='Quit') quit_item.connect('activate', Gtk.main_quit) if self.switching: quit_item.set_sensitive(False) menu.append(quit_item) - + menu.show_all() return menu - def switch_integrated(self, _): - self.switch_gpu('integrated') + def on_integrated_toggled(self, widget): + """Handle integrated mode toggle""" + selected_mode = self.target_mode if self.target_mode else self.boot_mode + if widget.get_active() and selected_mode != 'integrated': + self.switch_gpu('integrated') + elif not widget.get_active() and selected_mode == 'integrated': + # User unchecked the active mode - recheck it + widget.set_active(True) - def switch_hybrid(self, _): - self.switch_gpu('hybrid') + def on_hybrid_toggled(self, widget): + """Handle hybrid mode toggle""" + selected_mode = self.target_mode if self.target_mode else self.boot_mode + if widget.get_active() and selected_mode != 'hybrid': + self.switch_gpu('hybrid') + elif not widget.get_active() and selected_mode == 'hybrid': + # User unchecked the active mode - recheck it + widget.set_active(True) def switch_gpu(self, mode): """Switch GPU mode""" @@ -442,7 +555,7 @@ def switch_thread(): try: cmd = ['pkexec', 'envycontrol', '-s', mode] if mode == 'hybrid': - cmd.extend(['--rtd3', '2']) + cmd.extend(['--rtd3', '3']) result = subprocess.run( cmd, @@ -487,33 +600,41 @@ def switch_cancelled(self): def switch_complete(self, mode, success, error_msg): """Handle switch completion""" self.switching = False - + if success: logging.info(f"Successfully switched to {mode}") - self.current_mode = mode + self.set_target_mode(mode) self.update_icon() self.indicator.set_menu(self.build_menu()) - - notification = Notify.Notification.new( - "✓ GPU Switched Successfully!", - f"Switched to {mode.upper()} mode.\n\n⚠️ REBOOT NOW for changes to take effect!", - "dialog-warning" - ) - notification.set_urgency(Notify.Urgency.CRITICAL) - notification.set_timeout(10000) + + if self.has_pending_change(): + notification = Notify.Notification.new( + "✓ GPU Mode Changed", + f"⚠️ Reboot to finish the switch to {mode.upper()} mode.", + "dialog-warning" + ) + notification.set_urgency(Notify.Urgency.CRITICAL) + notification.set_timeout(10000) + else: + # Switched back to boot mode - no reboot needed + notification = Notify.Notification.new( + "✓ GPU Mode Restored", + f"Restored to {mode.upper()} mode. No reboot required.", + "dialog-information" + ) notification.show() else: logging.error(f"Failed to switch to {mode}: {error_msg}") self.update_icon() self.indicator.set_menu(self.build_menu()) - + notification = Notify.Notification.new( "✗ GPU Switch Failed", f"Error: {error_msg if error_msg else 'Command failed'}", "dialog-error" ) notification.show() - + return False def show_about(self, _):