├── cpu_controller_linux ├── __init__.py ├── cpu.png └── main.py ├── image.png ├── .gitattributes ├── setup.py ├── LICENSE ├── README.md └── .gitignore /cpu_controller_linux/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import CPUCoresController 2 | 3 | -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devansharora18/cpu-controller/HEAD/image.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /cpu_controller_linux/cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devansharora18/cpu-controller/HEAD/cpu_controller_linux/cpu.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | VERSION = '0.1.0' 4 | DESCRIPTION = 'Turn on/off CPU cores in Linux' 5 | LONG_DESCRIPTION = 'A package that allows you to turn on/off CPU cores in Linux to increase battery life' 6 | 7 | # Setting up 8 | setup( 9 | name="cpu_controller_linux", 10 | version=VERSION, 11 | author="Devansh Arora", 12 | author_email="hsnaved.reverse@gmail.com", 13 | description=DESCRIPTION, 14 | long_description_content_type="text/markdown", 15 | long_description=LONG_DESCRIPTION, 16 | packages=find_packages(), 17 | include_package_data=True, 18 | install_requires=['PyQt6'], 19 | keywords=['python', 'linux', 'cpu', 'battery life', 'cpu cores',], 20 | classifiers=[ 21 | "Intended Audience :: Developers", 22 | "Programming Language :: Python :: 3", 23 | "Operating System :: Unix", 24 | ], 25 | entry_points={ 26 | 'console_scripts': [ 27 | 'cpu-controller = cpu_controller_linux.main:main', 28 | ], 29 | }, 30 | package_data={'cpu_controller_linux': ['cpu.png']}, 31 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Devansh Arora 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CPU Cores Controller 2 | 3 | This is a PyQt6 application to control the state of CPU cores on a Linux system. The application allows you to enable or disable individual CPU cores or a range of CPU cores. 4 | 5 | ## Features 6 | 7 | - **Enable/Disable Individual Cores**: Click on the CPU core buttons to toggle their state. 8 | - **Enable/Disable Range of Cores**: Use the "Disable Range" and "Enable Range" buttons to specify a range of cores to disable or enable. 9 | - **Dynamic Core Detection**: Automatically detects the number of CPU cores on your system. 10 | 11 | ## Requirements 12 | 13 | - Python 3.x 14 | - PyQt6 15 | - Root privileges (sudo access) 16 | 17 | ## Installation 18 | 19 | 1. Clone the repository: 20 | ```sh 21 | git clone https://github.com/devansharora18/cpu-controller.git 22 | cd cpu-controller 23 | ``` 24 | 25 | 2. Install the required Python packages: 26 | ```sh 27 | pip install -r requirements.txt 28 | ``` 29 | 30 | 3. Run the application: 31 | ```sh 32 | sudo python3 main.py 33 | ``` 34 | 35 | ## Usage 36 | 37 | - **Toggle Individual Cores**: Click on the CPU core buttons to enable or disable them. 38 | - **Disable Range**: Click on the "Disable Range" button and enter the start and end core indices to disable. 39 | - **Enable Range**: Click on the "Enable Range" button and enter the start and end core indices to enable. 40 | 41 | ## Screenshots 42 | 43 | ![CPU Cores Controller](image.png) 44 | 45 | ## License 46 | 47 | This project is licensed under the MIT License. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ 153 | -------------------------------------------------------------------------------- /cpu_controller_linux/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from PyQt6.QtWidgets import QApplication, QWidget, QMessageBox, QGridLayout, QPushButton, QLabel, QVBoxLayout, QInputDialog, QHBoxLayout 3 | from PyQt6.QtCore import Qt, QSize 4 | from PyQt6.QtGui import QIcon, QFont 5 | import subprocess 6 | import pkg_resources 7 | import os 8 | 9 | os.environ["XDG_SESSION_TYPE"] = "xcb" 10 | 11 | 12 | class CPUCoresController(QWidget): 13 | def __init__(self): 14 | super().__init__() 15 | 16 | self.init_ui() 17 | 18 | def init_ui(self): 19 | icon_path = pkg_resources.resource_filename(__name__, 'cpu.png') 20 | self.setWindowTitle('CPU Cores Controller') 21 | self.setMinimumSize(600, 600) 22 | 23 | # Dynamically determine the number of CPU cores from the filesystem 24 | self.num_cores = self.detect_cores() 25 | 26 | self.layout = QVBoxLayout() 27 | 28 | # Create the range control buttons (Disable and Enable Range) 29 | self.range_control_layout = QHBoxLayout() 30 | self.disable_range_button = QPushButton("Disable Range") 31 | self.disable_range_button.setFont(QFont('Arial', 12, QFont.Weight.Bold)) 32 | self.disable_range_button.clicked.connect(self.disable_range) 33 | 34 | self.enable_range_button = QPushButton("Enable Range") 35 | self.enable_range_button.setFont(QFont('Arial', 12, QFont.Weight.Bold)) 36 | self.enable_range_button.clicked.connect(self.enable_range) 37 | 38 | self.range_control_layout.addWidget(self.disable_range_button) 39 | self.range_control_layout.addWidget(self.enable_range_button) 40 | 41 | self.layout.addLayout(self.range_control_layout) 42 | 43 | # Create the grid layout for CPU core buttons 44 | self.grid_layout = QGridLayout() 45 | 46 | self.core_buttons = [] 47 | for core in range(self.num_cores): 48 | button = QPushButton() 49 | button.setCheckable(True) 50 | button.setIcon(QIcon(icon_path)) # Replace with the path to your icon 51 | button.setIconSize(QSize(64, 64)) # Adjust the icon size 52 | button.clicked.connect(lambda _, core=core: self.toggle_core(core)) 53 | 54 | # Add label to display core number below the icon 55 | label = QLabel(f'CPU {core}') 56 | label.setAlignment(Qt.AlignmentFlag.AlignCenter) 57 | label.setFont(QFont('Arial', 10, QFont.Weight.Bold)) # Adjust font for a more luxurious look 58 | 59 | # Create a container widget to hold the button and label vertically 60 | container = QWidget() 61 | container_layout = QVBoxLayout(container) 62 | container_layout.addWidget(button) 63 | container_layout.addWidget(label) 64 | container_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) 65 | 66 | self.grid_layout.addWidget(container, core // 4, core % 4) 67 | self.core_buttons.append(button) 68 | 69 | self.layout.addLayout(self.grid_layout) 70 | self.setLayout(self.layout) 71 | 72 | # Set initial core states 73 | self.update_core_states() 74 | 75 | def detect_cores(self): 76 | cores = 0 77 | while os.path.exists(f'/sys/devices/system/cpu/cpu{cores}'): 78 | cores += 1 79 | return cores 80 | 81 | def toggle_core(self, core): 82 | file_path = f'/sys/devices/system/cpu/cpu{core}/online' 83 | 84 | try: 85 | current_state = int(open(file_path).read().strip()) 86 | new_state = 1 - current_state 87 | subprocess.run(['sudo', 'su', '-c', f'echo {new_state} > {file_path}']) 88 | self.update_core_states() 89 | except FileNotFoundError: 90 | print(f'File not found: {file_path}. Assuming core is online.') 91 | except Exception as e: 92 | print(f'An error occurred: {str(e)}') 93 | 94 | def update_core_states(self): 95 | for core, button in enumerate(self.core_buttons): 96 | if core == 0: 97 | button.setChecked(True) 98 | button.setStyleSheet("background-color: #4CAF50; color: white;") 99 | continue 100 | file_path = f'/sys/devices/system/cpu/cpu{core}/online' 101 | 102 | try: 103 | current_state = int(open(file_path).read().strip()) 104 | if current_state == 1: 105 | button.setChecked(True) 106 | button.setStyleSheet("background-color: #4CAF50; color: white;") 107 | else: 108 | button.setChecked(False) 109 | button.setStyleSheet("background-color: #D32F2F; color: white;") 110 | except FileNotFoundError: 111 | button.setChecked(False) 112 | button.setStyleSheet("background-color: #D32F2F; color: white;") 113 | 114 | def disable_range(self): 115 | try: 116 | # Get the range of cores to disable 117 | start_core, ok1 = QInputDialog.getInt(self, "Disable Range", "Enter start core index:", 0, 0, self.num_cores - 1) 118 | if not ok1: 119 | return 120 | 121 | end_core, ok2 = QInputDialog.getInt(self, "Disable Range", "Enter end core index:", start_core, start_core, self.num_cores - 1) 122 | if not ok2: 123 | return 124 | 125 | # Disable the specified range 126 | for core in range(start_core, end_core + 1): 127 | file_path = f'/sys/devices/system/cpu/cpu{core}/online' 128 | try: 129 | subprocess.run(['sudo', 'su', '-c', f'echo 0 > {file_path}']) 130 | except Exception as e: 131 | print(f'An error occurred while disabling CPU {core}: {str(e)}') 132 | 133 | self.update_core_states() 134 | 135 | except Exception as e: 136 | print(f'An error occurred: {str(e)}') 137 | 138 | def enable_range(self): 139 | try: 140 | # Get the range of cores to enable 141 | start_core, ok1 = QInputDialog.getInt(self, "Enable Range", "Enter start core index:", 0, 0, self.num_cores - 1) 142 | if not ok1: 143 | return 144 | 145 | end_core, ok2 = QInputDialog.getInt(self, "Enable Range", "Enter end core index:", start_core, start_core, self.num_cores - 1) 146 | if not ok2: 147 | return 148 | 149 | # Enable the specified range 150 | for core in range(start_core, end_core + 1): 151 | file_path = f'/sys/devices/system/cpu/cpu{core}/online' 152 | try: 153 | subprocess.run(['sudo', 'su', '-c', f'echo 1 > {file_path}']) 154 | except Exception as e: 155 | print(f'An error occurred while enabling CPU {core}: {str(e)}') 156 | 157 | self.update_core_states() 158 | 159 | except Exception as e: 160 | print(f'An error occurred: {str(e)}') 161 | 162 | def show_all_cpus_enabled_popup(self): 163 | msg_box = QMessageBox() 164 | msg_box.setWindowTitle("All CPUs Enabled") 165 | msg_box.setText("All CPU cores have been enabled.") 166 | msg_box.setIcon(QMessageBox.Icon.Information) 167 | msg_box.exec() 168 | 169 | 170 | def main(): 171 | app = QApplication(sys.argv) 172 | 173 | if subprocess.run(['sudo', '-v']).returncode != 0: 174 | print("Please run this script as root.") 175 | sys.exit(1) 176 | 177 | cpu_cores_controller = CPUCoresController() 178 | cpu_cores_controller.show() 179 | sys.exit(app.exec()) 180 | 181 | 182 | if __name__ == '__main__': 183 | main() 184 | --------------------------------------------------------------------------------