289 lines
12 KiB
Python
289 lines
12 KiB
Python
import gi
|
|
gi.require_version("Gtk", "3.0")
|
|
from gi.repository import GdkPixbuf, Gtk
|
|
|
|
import sys
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from io import StringIO
|
|
from os.path import isfile
|
|
from platform import python_version
|
|
from subprocess import getoutput, PIPE, run
|
|
|
|
from auto_cpufreq.core import distro_info, get_formatted_version, get_override, sysinfo
|
|
from auto_cpufreq.globals import GITHUB, IS_INSTALLED_WITH_AUR, IS_INSTALLED_WITH_SNAP
|
|
|
|
PKEXEC_ERROR = "Error executing command as another user: Not authorized\n\nThis incident has been reported.\n"
|
|
|
|
auto_cpufreq_stats_path = ("/var/snap/auto-cpufreq/current" if IS_INSTALLED_WITH_SNAP else "/var/run") + "/auto-cpufreq.stats"
|
|
|
|
def get_stats():
|
|
if isfile(auto_cpufreq_stats_path):
|
|
with open(auto_cpufreq_stats_path, "r") as file: stats = [line for line in (file.readlines() [-50:])]
|
|
return "".join(stats)
|
|
|
|
def get_version():
|
|
# snap package
|
|
if IS_INSTALLED_WITH_SNAP: return getoutput(r"echo \(Snap\) $SNAP_VERSION")
|
|
# aur package
|
|
elif IS_INSTALLED_WITH_AUR: return getoutput("pacman -Qi auto-cpufreq | grep Version")
|
|
else:
|
|
# source code (auto-cpufreq-installer)
|
|
try: return get_formatted_version()
|
|
except Exception as e:
|
|
print(repr(e))
|
|
pass
|
|
|
|
class RadioButtonView(Gtk.Box):
|
|
def __init__(self):
|
|
super().__init__(orientation=Gtk.Orientation.HORIZONTAL)
|
|
|
|
self.set_hexpand(True)
|
|
self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
|
|
self.label = Gtk.Label("Governor Override", name="bold")
|
|
|
|
self.default = Gtk.RadioButton.new_with_label_from_widget(None, "Default")
|
|
self.default.connect("toggled", self.on_button_toggled, "reset")
|
|
self.default.set_halign(Gtk.Align.END)
|
|
self.powersave = Gtk.RadioButton.new_with_label_from_widget(self.default, "Powersave")
|
|
self.powersave.connect("toggled", self.on_button_toggled, "powersave")
|
|
self.powersave.set_halign(Gtk.Align.END)
|
|
self.performance = Gtk.RadioButton.new_with_label_from_widget(self.default, "Performance")
|
|
self.performance.connect("toggled", self.on_button_toggled, "performance")
|
|
self.performance.set_halign(Gtk.Align.END)
|
|
|
|
# this keeps track of whether or not the button was toggled by the app or the user to prompt for authorization
|
|
self.set_by_app = True
|
|
self.set_selected()
|
|
|
|
self.pack_start(self.label, False, False, 0)
|
|
self.pack_start(self.default, True, True, 0)
|
|
self.pack_start(self.powersave, True, True, 0)
|
|
self.pack_start(self.performance, True, True, 0)
|
|
|
|
def on_button_toggled(self, button, override):
|
|
if button.get_active():
|
|
if not self.set_by_app:
|
|
result = run(f"pkexec auto-cpufreq --force={override}", shell=True, stdout=PIPE, stderr=PIPE)
|
|
if result.stderr.decode() == PKEXEC_ERROR: self.set_selected()
|
|
else: self.set_by_app = False
|
|
|
|
def set_selected(self):
|
|
override = get_override()
|
|
match override:
|
|
case "powersave": self.powersave.set_active(True)
|
|
case "performance": self.performance.set_active(True)
|
|
case "default":
|
|
# because this is the default button, it does not trigger the callback when set by the app
|
|
if self.set_by_app: self.set_by_app = False
|
|
self.default.set_active(True)
|
|
|
|
class CurrentGovernorBox(Gtk.Box):
|
|
def __init__(self):
|
|
super().__init__(spacing=25)
|
|
self.static = Gtk.Label(label="Current Governor", name="bold")
|
|
self.governor = Gtk.Label(label=getoutput("cpufreqctl.auto-cpufreq --governor").strip().split(" ")[0], halign=Gtk.Align.END)
|
|
|
|
self.pack_start(self.static, False, False, 0)
|
|
self.pack_start(self.governor, False, False, 0)
|
|
|
|
def refresh(self):
|
|
self.governor.set_label(getoutput("cpufreqctl.auto-cpufreq --governor").strip().split(" ")[0])
|
|
|
|
class SystemStatsLabel(Gtk.Label):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.refresh()
|
|
|
|
def refresh(self):
|
|
# change stdout and store label text to file-like object
|
|
old_stdout = sys.stdout
|
|
text = StringIO()
|
|
sys.stdout = text
|
|
distro_info()
|
|
sysinfo()
|
|
self.set_label(text.getvalue())
|
|
sys.stdout = old_stdout
|
|
|
|
class CPUFreqStatsLabel(Gtk.Label):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.refresh()
|
|
|
|
def refresh(self):
|
|
stats = get_stats().split("\n")
|
|
start = None
|
|
for i, line in enumerate(stats):
|
|
if line == ("-" * 28 + " CPU frequency scaling " + "-" * 28):
|
|
start = i
|
|
break
|
|
if start is not None:
|
|
del stats[:i]
|
|
del stats[-4:]
|
|
self.set_label("\n".join(stats))
|
|
|
|
class DropDownMenu(Gtk.MenuButton):
|
|
def __init__(self, parent):
|
|
super().__init__()
|
|
self.set_halign(Gtk.Align.END)
|
|
self.set_valign(Gtk.Align.START)
|
|
self.image = Gtk.Image.new_from_icon_name("open-menu-symbolic", Gtk.IconSize.LARGE_TOOLBAR)
|
|
self.add(self.image)
|
|
self.menu = self.build_menu(parent)
|
|
self.set_popup(self.menu)
|
|
|
|
def build_menu(self, parent):
|
|
menu = Gtk.Menu()
|
|
|
|
daemon = Gtk.MenuItem(label="Remove Daemon")
|
|
daemon.connect("activate", self._remove_daemon, parent)
|
|
menu.append(daemon)
|
|
|
|
about = Gtk.MenuItem(label="About")
|
|
about.connect("activate", self.about_dialog, parent)
|
|
menu.append(about)
|
|
|
|
menu.show_all()
|
|
return menu
|
|
|
|
def about_dialog(self, MenuItem, parent):
|
|
dialog = AboutDialog(parent)
|
|
response = dialog.run()
|
|
dialog.destroy()
|
|
|
|
def _remove_daemon(self, MenuItem, parent):
|
|
confirm = ConfirmDialog(parent, message="Are you sure you want to remove the daemon?")
|
|
response = confirm.run()
|
|
confirm.destroy()
|
|
if response == Gtk.ResponseType.YES:
|
|
try:
|
|
# run in thread to prevent GUI from hanging
|
|
with ThreadPoolExecutor() as executor:
|
|
kwargs = {"shell": True, "stdout": PIPE, "stderr": PIPE}
|
|
future = executor.submit(run, "pkexec auto-cpufreq --remove", **kwargs)
|
|
result = future.result()
|
|
assert result.stderr.decode() != PKEXEC_ERROR, Exception("Authorization was cancelled")
|
|
dialog = Gtk.MessageDialog(
|
|
transient_for=parent,
|
|
message_type=Gtk.MessageType.INFO,
|
|
buttons=Gtk.ButtonsType.OK,
|
|
text="Daemon successfully removed"
|
|
)
|
|
dialog.format_secondary_text("The app will now close. Please reopen to apply changes")
|
|
dialog.run()
|
|
dialog.destroy()
|
|
parent.destroy()
|
|
except Exception as e:
|
|
dialog = Gtk.MessageDialog(
|
|
transient_for=parent,
|
|
message_type=Gtk.MessageType.ERROR,
|
|
buttons=Gtk.ButtonsType.OK,
|
|
text="Daemon removal failed"
|
|
)
|
|
dialog.format_secondary_text(f"The following error occured:\n{e}")
|
|
dialog.run()
|
|
dialog.destroy()
|
|
|
|
class AboutDialog(Gtk.Dialog):
|
|
def __init__(self, parent):
|
|
super().__init__(title="About", transient_for=parent)
|
|
app_version = get_version()
|
|
self.box = self.get_content_area()
|
|
self.box.set_spacing(10)
|
|
self.add_button("Close", Gtk.ResponseType.CLOSE)
|
|
self.set_default_size(400, 350)
|
|
img_buffer = GdkPixbuf.Pixbuf.new_from_file_at_scale(
|
|
filename="/usr/local/share/auto-cpufreq/images/icon.png",
|
|
width=150,
|
|
height=150,
|
|
preserve_aspect_ratio=True
|
|
)
|
|
self.image = Gtk.Image.new_from_pixbuf(img_buffer)
|
|
self.title = Gtk.Label(label="auto-cpufreq", name="bold")
|
|
self.version = Gtk.Label(label=app_version)
|
|
self.python = Gtk.Label(label=f"Python {python_version()}")
|
|
self.github = Gtk.Label(label=GITHUB)
|
|
self.license = Gtk.Label(label="Licensed under LGPL3", name="small")
|
|
self.love = Gtk.Label(label="Made with <3", name="small")
|
|
|
|
self.box.pack_start(self.image, False, False, 0)
|
|
self.box.pack_start(self.title, False, False, 0)
|
|
self.box.pack_start(self.version, False, False, 0)
|
|
self.box.pack_start(self.python, False, False, 0)
|
|
self.box.pack_start(self.github, False, False, 0)
|
|
self.box.pack_start(self.license, False, False, 0)
|
|
self.box.pack_start(self.love, False, False, 0)
|
|
self.show_all()
|
|
|
|
class UpdateDialog(Gtk.Dialog):
|
|
def __init__(self, parent, current_version: str, latest_version: str):
|
|
super().__init__(title="Update Available", transient_for=parent)
|
|
self.box = self.get_content_area()
|
|
self.set_default_size(400, 100)
|
|
self.add_buttons("Update", Gtk.ResponseType.YES, "Cancel", Gtk.ResponseType.NO)
|
|
self.label = Gtk.Label(label="An update is available\n")
|
|
self.current_version = Gtk.Label(label=current_version + "\n")
|
|
self.latest_version = Gtk.Label(label=latest_version + "\n")
|
|
|
|
self.box.pack_start(self.label, True, False, 0)
|
|
self.box.pack_start(self.current_version, True, False, 0)
|
|
self.box.pack_start(self.latest_version, True, False, 0)
|
|
|
|
self.show_all()
|
|
|
|
class ConfirmDialog(Gtk.Dialog):
|
|
def __init__(self, parent, message: str):
|
|
super().__init__(title="Confirmation", transient_for=parent)
|
|
self.box = self.get_content_area()
|
|
self.set_default_size(400, 100)
|
|
self.add_buttons("Yes", Gtk.ResponseType.YES, "No", Gtk.ResponseType.NO)
|
|
self.label = Gtk.Label(label=message)
|
|
|
|
self.box.pack_start(self.label, True, False, 0)
|
|
|
|
self.show_all()
|
|
|
|
class DaemonNotRunningView(Gtk.Box):
|
|
def __init__(self, parent):
|
|
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=10, halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER)
|
|
|
|
self.label = Gtk.Label(label="auto-cpufreq daemon is not running. Please click the install button")
|
|
self.install_button = Gtk.Button.new_with_label("Install")
|
|
|
|
self.install_button.connect("clicked", self.install_daemon, parent)
|
|
|
|
self.pack_start(self.label, False, False, 0)
|
|
self.pack_start(self.install_button, False, False, 0)
|
|
|
|
def install_daemon(self, button, parent):
|
|
try:
|
|
# run in thread to prevent GUI from hanging
|
|
with ThreadPoolExecutor() as executor:
|
|
kwargs = {"shell": True, "stdout": PIPE, "stderr": PIPE}
|
|
future = executor.submit(run, "pkexec auto-cpufreq --install", **kwargs)
|
|
result = future.result()
|
|
assert result.stderr.decode() != PKEXEC_ERROR, Exception("Authorization was cancelled")
|
|
# enable for debug. causes issues if kept
|
|
# elif result.stderr is not None:
|
|
# raise Exception(result.stderr.decode())
|
|
dialog = Gtk.MessageDialog(
|
|
transient_for=parent,
|
|
message_type=Gtk.MessageType.INFO,
|
|
buttons=Gtk.ButtonsType.OK,
|
|
text="Daemon successfully installed"
|
|
)
|
|
dialog.format_secondary_text("The app will now close. Please reopen to apply changes")
|
|
dialog.run()
|
|
dialog.destroy()
|
|
parent.destroy()
|
|
except Exception as e:
|
|
dialog = Gtk.MessageDialog(
|
|
transient_for=parent,
|
|
message_type=Gtk.MessageType.ERROR,
|
|
buttons=Gtk.ButtonsType.OK,
|
|
text="Daemon install failed"
|
|
)
|
|
dialog.format_secondary_text(f"The following error occured:\n{e}")
|
|
dialog.run()
|
|
dialog.destroy()
|