diff --git a/auto-cpufreq-installer b/auto-cpufreq-installer index d3e6561..05f7fac 100755 --- a/auto-cpufreq-installer +++ b/auto-cpufreq-installer @@ -63,10 +63,18 @@ function install { python3 setup.py install --record files.txt mkdir -p /usr/local/share/auto-cpufreq/ cp -r scripts/ /usr/local/share/auto-cpufreq/ + cp -r images/ /usr/local/share/auto-cpufreq/ + cp images/icon.png /usr/share/pixmaps/auto-cpufreq.png + cp scripts/org.auto-cpufreq.pkexec.policy /usr/share/polkit-1/actions # this is necessary since we need this script before we can run auto-cpufreq itself cp scripts/auto-cpufreq-venv-wrapper /usr/local/bin/auto-cpufreq + cp scripts/start_app /usr/local/bin/auto-cpufreq-gtk chmod a+x /usr/local/bin/auto-cpufreq + chmod a+x /usr/local/bin/auto-cpufreq-gtk + + desktop-file-install --dir=/usr/share/applications scripts/auto-cpufreq-gtk.desktop + update-desktop-database /usr/share/applications } # First argument is the distro @@ -140,7 +148,7 @@ function tool_install { separator if [ -f /etc/debian_version ]; then detected_distro "Debian based" - apt install python3-dev python3-pip python3-venv python3-setuptools dmidecode -y + apt install python3-dev python3-pip python3-venv python3-setuptools dmidecode libgirepository1.0-dev libcairo2-dev -y completed complete_msg elif [ -f /etc/redhat-release ]; then @@ -170,12 +178,12 @@ elif [ -f /etc/os-release ];then opensuse) detected_distro "OpenSUSE" echo -e "\nDetected an OpenSUSE distribution\n\nSetting up Python environment\n" - zypper install -y python38 python3-pip python3-setuptools python3-devel gcc dmidecode + zypper install -y python38 python3-pip python3-setuptools python3-devel gcc dmidecode gobject-introspection-devel python3-cairo-devel completed ;; arch|manjaro|endeavouros|garuda|artix) detected_distro "Arch Linux based" - pacman -S --noconfirm --needed python python-pip python-setuptools base-devel dmidecode + pacman -S --noconfirm --needed python python-pip python-setuptools base-devel dmidecode gobject-introspection completed ;; void) @@ -208,12 +216,15 @@ function tool_remove { tool_proc_rm="/usr/local/bin/auto-cpufreq --remove" wrapper_script="/usr/local/bin/auto-cpufreq" + gui_wrapper_script="/usr/local/bin/auto-cpufreq-gtk" unit_file="/etc/systemd/system/auto-cpufreq.service" venv_path="/opt/auto-cpufreq" cpufreqctl="/usr/local/bin/cpufreqctl.auto-cpufreq" cpufreqctl_old="/usr/bin/cpufreqctl.auto-cpufreq" + desktop_file="/usr/share/applications/auto-cpufreq-gtk.desktop" + # stop any running auto-cpufreq argument (daemon/live/monitor) tool_arg_pids=($(pgrep -f "auto-cpufreq --")) for pid in "${tool_arg_pids[@]}"; do @@ -246,10 +257,14 @@ function tool_remove { [ -f $stats_file ] && rm $stats_file [ -f $unit_file ] && rm $unit_file [ -f $wrapper_script ] && rm $wrapper_script - + [ -f $gui_wrapper_script ] && rm $gui_wrapper_script + [ -f $cpufreqctl ] && rm $cpufreqctl [ -f $cpufreqctl_old ] && rm $cpufreqctl_old + [ -f $desktop_file ] && rm $desktop_file + update-desktop-database /usr/share/applications + # remove python virtual environment rm -rf "${venv_path}" diff --git a/auto_cpufreq/core.py b/auto_cpufreq/core.py index 24c71ef..99397a9 100755 --- a/auto_cpufreq/core.py +++ b/auto_cpufreq/core.py @@ -29,6 +29,9 @@ from auto_cpufreq.power_helper import * warnings.filterwarnings("ignore") +# add path to auto-cpufreq executables for GUI +os.environ["PATH"] += ":/usr/local/bin" + # ToDo: # - replace get system/CPU load from: psutil.getloadavg() | available in 5.6.2) diff --git a/auto_cpufreq/gui/__init__.py b/auto_cpufreq/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/auto_cpufreq/gui/app.py b/auto_cpufreq/gui/app.py new file mode 100644 index 0000000..635cd9b --- /dev/null +++ b/auto_cpufreq/gui/app.py @@ -0,0 +1,81 @@ +import gi + +gi.require_version("Gtk", "3.0") + +from gi.repository import Gtk, GLib, Gdk, Gio, GdkPixbuf + +import os +import sys + +sys.path.append("../") +from auto_cpufreq.core import is_running +from auto_cpufreq.gui.objects import RadioButtonView, SystemStatsLabel, CPUFreqStatsLabel, CurrentGovernorBox, DropDownMenu, DaemonNotRunningView + +CSS_FILE = "/usr/local/share/auto-cpufreq/scripts/style.css" + +HBOX_PADDING = 20 + +class ToolWindow(Gtk.Window): + def __init__(self): + super().__init__(title="auto-cpufreq") + self.set_default_size(600, 480) + self.set_border_width(10) + self.set_resizable(False) + self.load_css() + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(filename="/usr/local/share/auto-cpufreq/images/icon.png", width=500, height=500, preserve_aspect_ratio=True) + self.set_icon(pixbuf) + self.build() + + def main(self): + # self.vbox_top = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + # self.vbox_top.set_valign(Gtk.Align.CENTER) + # self.vbox_top.set_halign(Gtk.Align.CENTER) + #self.add(self.vbox_top) + + # Main HBOX + self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=HBOX_PADDING) + + self.systemstats = SystemStatsLabel() + self.hbox.pack_start(self.systemstats, False, False, 0) + self.add(self.hbox) + + self.vbox_right = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=52) + + self.menu = DropDownMenu(self) + self.hbox.pack_end(self.menu, False, False, 0) + + self.currentgovernor = CurrentGovernorBox() + self.vbox_right.pack_start(self.currentgovernor, False, False, 0) + self.vbox_right.pack_start(RadioButtonView(), False, False, 0) + + self.cpufreqstats = CPUFreqStatsLabel() + self.vbox_right.pack_start(self.cpufreqstats, False, False, 0) + + self.hbox.pack_start(self.vbox_right, False, False, 0) + + + GLib.timeout_add_seconds(5, self.refresh) + + def daemon_not_running(self): + self.box = DaemonNotRunningView(self) + self.add(self.box) + + def build(self): + if is_running("auto-cpufreq", "--daemon"): + self.main() + else: + self.daemon_not_running() + + def load_css(self): + screen = Gdk.Screen.get_default() + self.gtk_provider = Gtk.CssProvider() + self.gtk_context = Gtk.StyleContext() + self.gtk_context.add_provider_for_screen(screen, self.gtk_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + self.gtk_provider.load_from_file(Gio.File.new_for_path(CSS_FILE)) + + def refresh(self): + self.systemstats.refresh() + self.currentgovernor.refresh() + self.cpufreqstats.refresh() + return True + diff --git a/auto_cpufreq/gui/objects.py b/auto_cpufreq/gui/objects.py new file mode 100644 index 0000000..af0901a --- /dev/null +++ b/auto_cpufreq/gui/objects.py @@ -0,0 +1,292 @@ +import gi + +gi.require_version("Gtk", "3.0") + +from gi.repository import Gtk, GdkPixbuf + +import sys +import os +import platform as pl + +sys.path.append("../../") +from subprocess import getoutput, run, PIPE +from auto_cpufreq.core import sysinfo, distro_info, set_override, get_override, get_formatted_version, dist_name, deploy_daemon, remove_daemon + +from io import StringIO + +PKEXEC_ERROR = "Error executing command as another user: Not authorized\n\nThis incident has been reported.\n" + +if os.getenv("PKG_MARKER") == "SNAP": + auto_cpufreq_stats_path = "/var/snap/auto-cpufreq/current/auto-cpufreq.stats" +else: + auto_cpufreq_stats_path = "/var/run/auto-cpufreq.stats" + + +def get_stats(): + if os.path.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 os.getenv("PKG_MARKER") == "SNAP": + return getoutput("echo \(Snap\) $SNAP_VERSION") + # aur package + elif dist_name in ["arch", "manjaro", "garuda"]: + aur_pkg_check = run("pacman -Qs auto-cpufreq > /dev/null", shell=True) + if aur_pkg_check == 1: + return get_formatted_version() + else: + 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) + + #self.pack_start(self.label, False, False, 0) + #self.pack_start(self.hbox, False, False, 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: + result = run("pkexec auto-cpufreq --remove", shell=True, stdout=PIPE, stderr=PIPE) + if result.stderr.decode() == PKEXEC_ERROR: + raise Exception("Authorization was cancelled") + dialog = Gtk.MessageDialog( + transient_for=parent, + message_type=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.OK, + text="Daemon succesfully 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_homogeneous(True) + 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 {pl.python_version()}") + self.github = Gtk.Label(label="https://github.com/AdnanHodzic/auto-cpufreq") + 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 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: + result = run("pkexec auto-cpufreq --install", shell=True, stdout=PIPE, stderr=PIPE) + if result.stderr.decode() == PKEXEC_ERROR: + raise Exception("Authorization was cancelled") + dialog = Gtk.MessageDialog( + transient_for=parent, + message_type=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.OK, + text="Daemon succesfully 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() \ No newline at end of file diff --git a/auto_cpufreq/gui/tray.py b/auto_cpufreq/gui/tray.py new file mode 100644 index 0000000..de0495d --- /dev/null +++ b/auto_cpufreq/gui/tray.py @@ -0,0 +1,32 @@ +import gi + +gi.require_version("Gtk", "3.0") + +from gi.repository import Gtk, AppIndicator3 as appindicator + +from subprocess import run + +def main(): + indicator = appindicator.Indicator.new("auto-cpufreq-tray", "network-idle-symbolic", appindicator.IndicatorCategory.APPLICATION_STATUS) + indicator.set_status(appindicator.IndicatorStatus.ACTIVE) + indicator.set_menu(build_menu()) + Gtk.main() + +def build_menu(): + menu = Gtk.Menu() + + program = Gtk.MenuItem("auto-cpufreq") + program.connect("activate", open_app) + menu.append(program) + + _quit = Gtk.MenuItem("Quit") + _quit.connect("activate", Gtk.main_quit) + menu.append(_quit) + menu.show_all() + return menu + +def open_app(MenuItem): + run("sudo -E python app.py", shell=True) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/bin/auto-cpufreq-gtk b/bin/auto-cpufreq-gtk new file mode 100644 index 0000000..17f757d --- /dev/null +++ b/bin/auto-cpufreq-gtk @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +import sys + +sys.path.append("../") + +import gi +gi.require_version("Gtk", "3.0") + +from gi.repository import Gtk, GLib +from auto_cpufreq.gui.app import ToolWindow + +if __name__ == "__main__": + win = ToolWindow() + win.connect("destroy", Gtk.main_quit) + win.show_all() + GLib.set_prgname("auto-cpufreq") + Gtk.main() \ No newline at end of file diff --git a/images/icon.png b/images/icon.png new file mode 100644 index 0000000..a187457 Binary files /dev/null and b/images/icon.png differ diff --git a/requirements.txt b/requirements.txt index e61d1a4..f492cac 100755 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ psutil click distro requests +PyGObject \ No newline at end of file diff --git a/scripts/auto-cpufreq-gtk.desktop b/scripts/auto-cpufreq-gtk.desktop new file mode 100644 index 0000000..fdc49a7 --- /dev/null +++ b/scripts/auto-cpufreq-gtk.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=auto-cpufreq +Exec=auto-cpufreq-gtk +Type=Application +Terminal=false +Icon=auto-cpufreq +StartupWMClass=app.py +Categories=System; \ No newline at end of file diff --git a/scripts/org.auto-cpufreq.pkexec.policy b/scripts/org.auto-cpufreq.pkexec.policy new file mode 100644 index 0000000..d59e34b --- /dev/null +++ b/scripts/org.auto-cpufreq.pkexec.policy @@ -0,0 +1,19 @@ + + + + + Run auto-cpufreq command + Authentication is required to run auto-cpufreq + auto-cpufreq + + auth_admin + auth_admin + auth_admin + + /opt/auto-cpufreq/venv/bin/auto-cpufreq + + + + \ No newline at end of file diff --git a/scripts/start_app b/scripts/start_app new file mode 100644 index 0000000..02c1fbc --- /dev/null +++ b/scripts/start_app @@ -0,0 +1,18 @@ +#!/usr/bin/sh + +# load python virtual environment +venv_dir=/opt/auto-cpufreq/venv +. "${venv_dir}/bin/activate" +python_command="${venv_dir}/bin/python ${venv_dir}/bin/auto-cpufreq-gtk" + +# if [ "$XDG_SESSION_TYPE" = "wayland" ] ; then +# # necessary for running on wayland +# xhost +SI:localuser:root +# pkexec ${python_command} +# xhost -SI:localuser:root +# xhost +# else +# pkexec ${python_command} +# fi + +${python_command} \ No newline at end of file diff --git a/scripts/style.css b/scripts/style.css new file mode 100644 index 0000000..e56f3cd --- /dev/null +++ b/scripts/style.css @@ -0,0 +1,12 @@ +label{ + /*font-family: Noto Sans;*/ + font-size: 15px; +} + +#bold{ + font-weight: bold; +} + +#small{ + font-size: 12px; +} diff --git a/setup.py b/setup.py index 3126d95..8417193 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ setup( author="Adnan Hodzic", author_email="adnan@hodzic.org", url="https://github.com/AdnanHodzic/auto-cpufreq", - packages=["auto_cpufreq"], + packages=["auto_cpufreq", "auto_cpufreq/gui"], install_requires=read("requirements.txt"), include_package_data=True, zip_safe=True, @@ -40,5 +40,5 @@ setup( "Intended Audience :: Developers", "Operating System :: POSIX :: Linux" "Environment :: Console" "Natural Language :: English", ], - scripts=["bin/auto-cpufreq"], + scripts=["bin/auto-cpufreq", "bin/auto-cpufreq-gtk"], ) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 27e0c4c..ebe16bc 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -24,6 +24,8 @@ parts: build-packages: - gcc - python3-dev + - libgirepository1.0-dev + - libcairo2-dev stage-packages: - coreutils - dmidecode @@ -58,6 +60,18 @@ apps: - system-observe - hardware-observe - etc-auto-cpufreq-conf + auto-cpufreq-gtk: + command: bin/auto-cpufreq-gtk + environment: + PYTHONPATH: $SNAP/usr/lib/python3/site-packages:$SNAP/usr/lib/python3/dist-packages:$PYTHONPATH + LC_ALL: C.UTF-8 + LANG: C.UTF-8 + PKG_MARKER: SNAP + plugs: + - cpu-control + - system-observe + - hardware-observe + - etc-auto-cpufreq-conf service: command: usr/bin/snapdaemon plugs: