#!/usr/bin/env python3 import sys import subprocess from PyQt6.QtWidgets import QApplication, QSystemTrayIcon, QMenu, QMessageBox from PyQt6.QtGui import QIcon, QPixmap, QPainter, QColor, QAction, QPen from PyQt6.QtCore import Qt, QTimer, QRect MODE_LABELS = { 'hybrid': 'Hybrid (AMD + NVIDIA)', 'dgpu': 'NVIDIA only', 'unknown': 'Unknown', } SWITCH_LABELS = { 'hybrid': 'Switch to Hybrid (AMD + NVIDIA)', 'dgpu': 'Switch to NVIDIA only', } AMD_RED = QColor('#EF5350') NV_GREEN = QColor('#66BB6A') GREY = QColor('#9E9E9E') BADGE = QColor('#FFA726') def get_active_gpu(): """Returns 'dgpu' if any user process has /dev/nvidia0 open, else 'igpu'. runtime_status is unreliable here because nvidia-drm.modeset=1 keeps the PCI device active regardless of user-space usage.""" try: r = subprocess.run( ['fuser', '/dev/nvidia0'], capture_output=True, timeout=3, ) return 'dgpu' if r.returncode == 0 else 'igpu' except Exception: pass return 'igpu' def get_mode(): try: r = subprocess.run( ['legion_cli', '--donotexpecthwmon', 'hybrid-mode-status'], capture_output=True, text=True, timeout=5, ) out = r.stdout + r.stderr if 'True' in out: return 'hybrid' if 'False' in out: return 'dgpu' except Exception: pass return 'unknown' def switch_mode(target): cmd = 'hybrid-mode-enable' if target == 'hybrid' else 'hybrid-mode-disable' r = subprocess.run( ['pkexec', '/usr/bin/legion_cli', '--donotexpecthwmon', cmd], capture_output=True, text=True, timeout=30, ) return r.returncode == 0 def _draw_chip(painter, color, clip=None): if clip: painter.save() painter.setClipRect(clip) pen = QPen(color, 1.5, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin) painter.setPen(pen) painter.setBrush(Qt.BrushStyle.NoBrush) # Body painter.drawRoundedRect(5, 5, 12, 12, 2, 2) # Left pins painter.drawRect(2, 8, 3, 2) painter.drawRect(2, 12, 3, 2) # Right pins painter.drawRect(17, 8, 3, 2) painter.drawRect(17, 12, 3, 2) # Top pins painter.drawRect(8, 2, 2, 3) painter.drawRect(12, 2, 2, 3) # Bottom pins painter.drawRect(8, 17, 2, 3) painter.drawRect(12, 17, 2, 3) if clip: painter.restore() def make_icon(mode, pending=False, active_gpu='igpu'): size = 22 px = QPixmap(size, size) px.fill(Qt.GlobalColor.transparent) p = QPainter(px) p.setRenderHint(QPainter.RenderHint.Antialiasing) if mode == 'hybrid': fill = QColor(NV_GREEN if active_gpu == 'dgpu' else AMD_RED) fill.setAlpha(90) p.setPen(Qt.PenStyle.NoPen) p.setBrush(fill) p.drawRoundedRect(5, 5, 12, 12, 2, 2) _draw_chip(p, AMD_RED, QRect(0, 0, 11, size)) _draw_chip(p, NV_GREEN, QRect(11, 0, 11, size)) elif mode == 'dgpu': _draw_chip(p, NV_GREEN) else: _draw_chip(p, GREY) if pending: p.setPen(Qt.PenStyle.NoPen) p.setBrush(BADGE) p.drawEllipse(15, 0, 6, 6) p.end() return QIcon(px) class GpuModeTray: def __init__(self): self.app = QApplication(sys.argv) self.app.setQuitOnLastWindowClosed(False) self.active_mode = get_mode() self.pending_mode = None self.active_gpu = get_active_gpu() if self.active_mode == 'hybrid' else None self.tray = QSystemTrayIcon(make_icon(self.active_mode, active_gpu=self.active_gpu)) self._build_menu() self.tray.setContextMenu(self.menu) self.tray.show() self.timer = QTimer() self.timer.timeout.connect(self._refresh) self.timer.start(30_000) self.gpu_timer = QTimer() self.gpu_timer.timeout.connect(self._refresh_active_gpu) self.gpu_timer.start(5_000) def _build_menu(self): self.menu = QMenu() self.status_item = QAction() self.status_item.setEnabled(False) self.menu.addAction(self.status_item) self.menu.addSeparator() self.hybrid_action = QAction() self.hybrid_action.triggered.connect(lambda: self._request_switch('hybrid')) self.menu.addAction(self.hybrid_action) self.dgpu_action = QAction() self.dgpu_action.triggered.connect(lambda: self._request_switch('dgpu')) self.menu.addAction(self.dgpu_action) self.sep_dgpu_procs = self.menu.addSeparator() self.dgpu_procs_action = QAction('Show dGPU processes') self.dgpu_procs_action.triggered.connect(self._show_dgpu_processes) self.menu.addAction(self.dgpu_procs_action) self.sep_pending = self.menu.addSeparator() self.cancel_action = QAction() self.cancel_action.triggered.connect(self._cancel_switch) self.menu.addAction(self.cancel_action) self.reboot_action = QAction('Reboot now') self.reboot_action.triggered.connect(lambda: subprocess.run(['systemctl', 'reboot'])) self.menu.addAction(self.reboot_action) self.menu.addSeparator() self.exit_action = QAction('Exit') self.exit_action.triggered.connect(self.app.quit) self.menu.addAction(self.exit_action) self._sync_ui() def _sync_ui(self): pending = self.pending_mode is not None active_label = MODE_LABELS[self.active_mode] if pending: pending_label = MODE_LABELS[self.pending_mode] self.status_item.setText(f'GPU mode: {active_label}') self.tray.setToolTip(f'GPU mode: {active_label} → {pending_label} (pending reboot)') else: self.status_item.setText(f'GPU mode: {active_label}') self.tray.setToolTip(f'GPU mode: {active_label}') self.tray.setIcon(make_icon(self.active_mode, pending=pending, active_gpu=self.active_gpu)) # Switch action labels and enabled state for key, action in [('hybrid', self.hybrid_action), ('dgpu', self.dgpu_action)]: if pending and key == self.pending_mode: action.setText(f'{MODE_LABELS[key]} (pending reboot)') action.setEnabled(False) elif pending: action.setText(SWITCH_LABELS[key]) action.setEnabled(False) else: action.setText(SWITCH_LABELS[key]) action.setEnabled(self.active_mode != key) in_hybrid = self.active_mode == 'hybrid' self.sep_dgpu_procs.setVisible(in_hybrid) self.dgpu_procs_action.setVisible(in_hybrid) self.sep_pending.setVisible(pending) self.cancel_action.setVisible(pending) self.reboot_action.setVisible(pending) if pending: self.cancel_action.setText(f'Cancel switch to {MODE_LABELS[self.pending_mode]}') def _show_dgpu_processes(self): try: r = subprocess.run( ['nvidia-smi', '--query-compute-apps=pid,name,used_memory', '--format=csv,noheader'], capture_output=True, text=True, timeout=5, ) text = r.stdout.strip() if not text: text = 'No processes currently using the dGPU.' except Exception as e: text = f'Failed to query nvidia-smi:\n{e}' QMessageBox.information(None, 'dGPU processes', text) def _refresh_active_gpu(self): if self.active_mode != 'hybrid': return gpu = get_active_gpu() if gpu != self.active_gpu: self.active_gpu = gpu self._sync_ui() def _refresh(self): mode = get_mode() if mode != self.active_mode: self.active_mode = mode self.active_gpu = get_active_gpu() if mode == 'hybrid' else None self.pending_mode = None self._sync_ui() def _request_switch(self, target): label = MODE_LABELS[target] if QMessageBox.question( None, 'Switch GPU mode', f'Switch to {label}?

A reboot is required for the change to take effect.', QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel, ) != QMessageBox.StandardButton.Ok: return if not switch_mode(target): QMessageBox.warning(None, 'GPU mode', 'Failed to switch GPU mode.\nCheck polkit authentication.') return self.pending_mode = target self._sync_ui() self.tray.showMessage( 'GPU mode switch pending', f'Will switch to {label} after reboot. Use the tray menu to cancel.', QSystemTrayIcon.MessageIcon.Information, 6000, ) if QMessageBox.question( None, 'Reboot required', f'GPU mode will switch to {label} after reboot.

Reboot now?', QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) == QMessageBox.StandardButton.Yes: subprocess.run(['systemctl', 'reboot']) def _cancel_switch(self): if not self.pending_mode: return if not switch_mode(self.active_mode): QMessageBox.warning(None, 'GPU mode', 'Failed to revert GPU mode.\nCheck polkit authentication.') return self.pending_mode = None self._sync_ui() self.tray.showMessage( 'GPU mode switch cancelled', 'No changes will be applied on reboot.', QSystemTrayIcon.MessageIcon.Information, 4000, ) def run(self): sys.exit(self.app.exec()) if __name__ == '__main__': GpuModeTray().run()