diff --git a/README.md b/README.md index d3e09ec..f706804 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ A graphical user interface for configuring GPU related environment variables and - CPU Management - Governor Selection: Choose from available CPU governors + - Adjust the maximum and minimum CPU frequencies within the permitted range. - Scheduler Configuration: Select CPU pluggable schedulers (requires [scx](https://github.com/sched-ext/scx) and `Linux Kernel >= 6.12` or a `Custom Patched Kernel`) - GPU Configuration - Mesa Drivers: Configure Mesa Drivers specific environment variables @@ -37,24 +38,64 @@ A graphical user interface for configuring GPU related environment variables and - Kernel Configuration - Choose /proc/sys/vm/compaction_proactiveness value - Choose /proc/sys/vm/watermark_boost_factor value + - Choose /proc/sys/vm/watermark_scale_factor value + - Choose /proc/sys/vm/extfrag_threshold value + - Choose /proc/sys/vm/compact_unevictable_allowed value + - Choose /proc/sys/vm/defrag_mode value - Choose /proc/sys/vm/min_free_kbytes value + - Choose /proc/sys/vm/overcommit_memory value + - Choose /proc/sys/vm/overcommit_ratio value + - Choose /proc/sys/vm/admin_reserve_kbytes value + - Choose /proc/sys/vm/user_reserve_kbytes value - Choose /proc/sys/vm/max_map_count value + - Choose /proc/sys/vm/mmap_min_addr value + - Choose /proc/sys/vm/page_lock_unfairness value - Choose /proc/sys/vm/swappiness value + - Choose /proc/sys/vm/page-cluster value + - Choose /proc/sys/vm/vfs_cache_pressure value + - Choose /proc/sys/vm/percpu_pagelist_high_fraction value + - Choose /proc/sys/vm/zone_reclaim_mode value + - Choose /proc/sys/vm/min_unmapped_ratio value + - Choose /proc/sys/vm/min_slab_ratio value + - Choose /proc/sys/vm/numa_stat value + - Choose /proc/sys/vm/oom_kill_allocating_task value + - Choose /proc/sys/vm/oom_dump_tasks value + - Choose /proc/sys/vm/panic_on_oom value - Choose /proc/sys/vm/dirty_ratio value - Choose /proc/sys/vm/dirty_background_ratio value + - Choose /proc/sys/vm/dirty_bytes value + - Choose /proc/sys/vm/dirty_background_bytes value - Choose /proc/sys/vm/dirty_expire_centisecs value - Choose /proc/sys/vm/dirty_writeback_centisecs value - - Choose /proc/sys/vm/vfs_cache_pressure value + - Choose /proc/sys/vm/dirtytime_expire_seconds value + - Choose /proc/sys/vm/laptop_mode value + - Choose /proc/sys/vm/nr_hugepages value + - Choose /proc/sys/vm/nr_overcommit_hugepages value + - Choose /proc/sys/vm/hugetlb_optimize_vmemmap value + - Choose /proc/sys/vm/stat_interval value - Choose /sys/kernel/mm/transparent_hugepage/enabled value - Choose /sys/kernel/mm/transparent_hugepage/shmem_enabled value - Choose /sys/kernel/mm/transparent_hugepage/defrag value - - Choose /proc/sys/vm/zone_reclaim_mode value - - Choose /proc/sys/vm/page_lock_unfairness value + - Choose /sys/class/rtc/rtc0/max_user_freq value + - Choose /proc/sys/kernel/numa_balancing value + - Choose /proc/sys/kernel/randomize_va_space value + - Choose /proc/sys/kernel/perf_event_paranoid value - Choose /proc/sys/kernel/sched_cfs_bandwidth_slice_us value - Choose /proc/sys/kernel/sched_autogroup_enabled value + - Choose /proc/sys/kernel/sched_rt_runtime_us value + - Choose /proc/sys/kernel/sched_rt_period_us value + - Choose /proc/sys/kernel/sched_schedstats value + - Choose /proc/sys/kernel/timer_migration value - Choose /proc/sys/kernel/watchdog value - Choose /proc/sys/kernel/nmi_watchdog value - - Choose /proc/sys/vm/laptop_mode value + - Choose /proc/sys/kernel/hung_task_timeout_secs value + - Choose /proc/sys/kernel/pid_max value + - Choose /proc/sys/fs/file-max value + - Choose /proc/sys/net/core/rmem_max value + - Choose /proc/sys/net/core/wmem_max value + - Choose /proc/sys/net/ipv4/tcp_fastopen value + - Choose /proc/sys/net/ipv4/tcp_window_scaling value + - Choose /proc/sys/net/ipv4/tcp_timestamps value - Launch Options: add custom Launch Options to the `volt` that will be passed to the program executed, example: ``` @@ -196,6 +237,7 @@ Documentation used: - [NVIDIA 570 Drivers - Documentation](https://download.nvidia.com/XFree86/Linux-x86_64/570.153.02/README/openglenvvariables.html) - [NVIDIA 470 Drivers - Documentation](https://download.nvidia.com/XFree86/Linux-x86_64/470.256.02/README/openglenvvariables.html) - [NVIDIA 390 Drivers - Documentation](https://download.nvidia.com/XFree86/Linux-x86_64/390.157/README/openglenvvariables.html) +- [Linux Kernel - Subsystem Documentation](https://docs.kernel.org/subsystem-apis.html) ## Contributing: diff --git a/images/1.png b/images/1.png index cf21ef0..9c3a691 100644 Binary files a/images/1.png and b/images/1.png differ diff --git a/images/2.png b/images/2.png index 00c4729..4355ff3 100644 Binary files a/images/2.png and b/images/2.png differ diff --git a/images/3.png b/images/3.png index 0e95bdb..5f3e05b 100644 Binary files a/images/3.png and b/images/3.png differ diff --git a/scripts/volt-helper b/scripts/volt-helper index 07c80c2..e136d9c 100755 --- a/scripts/volt-helper +++ b/scripts/volt-helper @@ -10,6 +10,20 @@ apply_governor() { done } +apply_max_freq() { + local max_freq="$1" + for CPU_PATH in /sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq; do + echo "$max_freq" > "$CPU_PATH" + done +} + +apply_min_freq() { + local min_freq="$1" + for CPU_PATH in /sys/devices/system/cpu/cpu*/cpufreq/scaling_min_freq; do + echo "$min_freq" > "$CPU_PATH" + done +} + terminate_existing_schedulers() { local scheduler_pid=$(pgrep -f '^scx_' 2>/dev/null | head -n1) @@ -52,19 +66,33 @@ manage_cpu() { local governor="" local scheduler="" + local max_freq="" + local min_freq="" for arg in "${cpu_args[@]}"; do if [[ "$arg" == governor:* ]]; then governor="${arg#governor:}" elif [[ "$arg" == scheduler:* ]]; then scheduler="${arg#scheduler:}" + elif [[ "$arg" == max_freq:* ]]; then + max_freq="${arg#max_freq:}" + elif [[ "$arg" == min_freq:* ]]; then + min_freq="${arg#min_freq:}" fi done if [ -n "$governor" ]; then apply_governor "$governor" fi + + if [ -n "$min_freq" ]; then + apply_min_freq "$min_freq" + fi + if [ -n "$max_freq" ]; then + apply_max_freq "$max_freq" + fi + if [ -n "$scheduler" ]; then handle_scheduler "$scheduler" fi diff --git a/src/about.py b/src/about.py index 11ada67..b1914b5 100644 --- a/src/about.py +++ b/src/about.py @@ -13,7 +13,7 @@ def get_about_info(): {"label": "Description", "text": "Simple GUI program for modifying and creating the `volt` script and more. Providing an intuitive interface for configuration management, with the objective of getting the maximum performance posible of a PC"}, {"label": "License", "text": "GPL-3.0 License"}, {"label": "Author", "text": "pythonlover02"}, - {"label": "Version", "text": "1.1.1"}, + {"label": "Version", "text": "1.2.0"}, ] @staticmethod diff --git a/src/config.py b/src/config.py index 2fa1546..1bb23c6 100644 --- a/src/config.py +++ b/src/config.py @@ -45,6 +45,8 @@ def save_config(cpu_widgets, gpu_widgets, kernel_widgets, disk_widgets, profile_ config['CPU'] = { 'governor': cpu_widgets['gov_combo'].currentText(), + 'max_freq': cpu_widgets['max_freq_combo'].currentText(), + 'min_freq': cpu_widgets['min_freq_combo'].currentText(), 'scheduler': cpu_widgets['sched_combo'].currentText() } @@ -106,6 +108,8 @@ def load_config(cpu_widgets, gpu_widgets, kernel_widgets, disk_widgets, profile_ if 'CPU' in config: cpu_widgets['gov_combo'].setCurrentText(config['CPU'].get('governor', 'unset')) + cpu_widgets['max_freq_combo'].setCurrentText(config['CPU'].get('max_freq', 'unset')) + cpu_widgets['min_freq_combo'].setCurrentText(config['CPU'].get('min_freq', 'unset')) cpu_widgets['sched_combo'].setCurrentText(config['CPU'].get('scheduler', 'unset')) if 'Mesa' in config and 'mesa' in gpu_widgets: diff --git a/src/cpu.py b/src/cpu.py index 693a250..925fa3c 100644 --- a/src/cpu.py +++ b/src/cpu.py @@ -10,10 +10,21 @@ class CPUManager: Main CPU management class that handles CPU governors and schedulers. """ - CPU_GOVERNOR_PATH = "/sys/devices/system/cpu/cpu*/cpufreq/scaling_governor" SCHEDULER_SEARCH_PATHS = ["/usr/bin/", "/usr/local/bin/"] BASE_SCHEDULERS = ["unset", "none"] + @staticmethod + def get_available_setting(setting_path): + """ + Check if a CPU setting file is available. + """ + try: + with open(setting_path, "r") as f: + f.read() + return True + except Exception: + return False + @staticmethod def get_available_governors(): """ @@ -25,7 +36,27 @@ def get_available_governors(): return ["unset"] + governors except Exception as e: print(f"Error reading available governors: {e}") + return ["unset"] + + @staticmethod + def get_cpuinfo_min_freq(): + """ + Gets the minimum possible CPU frequency in MHz. + """ + if not CPUManager.get_available_setting("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq"): + return None + with open("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq", "r") as f: + return int(f.read().strip()) // 1000 + + @staticmethod + def get_cpuinfo_max_freq(): + """ + Gets the maximum possible CPU frequency in MHz. + """ + if not CPUManager.get_available_setting("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq"): return None + with open("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq", "r") as f: + return int(f.read().strip()) // 1000 @staticmethod def get_available_schedulers(): @@ -58,6 +89,30 @@ def get_current_governor(): print(f"Error reading current governor: {e}") return None + @staticmethod + def get_current_scaling_min_freq(): + """ + Gets the current scaling minimum CPU frequency in MHz. + """ + try: + with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq", "r") as f: + return int(f.read().strip()) // 1000 + except (IOError, ValueError, FileNotFoundError) as e: + print(f"Error reading scaling_min_freq: {e}") + return None + + @staticmethod + def get_current_scaling_max_freq(): + """ + Gets the current scaling maximum CPU frequency in MHz. + """ + try: + with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq", "r") as f: + return int(f.read().strip()) // 1000 + except (IOError, ValueError, FileNotFoundError) as e: + print(f"Error reading scaling_max_freq: {e}") + return None + @staticmethod def get_current_scheduler(): """ @@ -99,7 +154,7 @@ def create_cpu_tab(): widgets = {} gov_layout = QHBoxLayout() - gov_label = QLabel("CPU Governor:") + gov_label = QLabel("Governor:") gov_label.setWordWrap(True) gov_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) @@ -111,6 +166,11 @@ def create_cpu_tab(): widgets['available_governors'] = available_governors + gov_accessible = CPUManager.get_available_setting("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor") + if not gov_accessible: + widgets['gov_combo'].setEnabled(False) + widgets['gov_combo'].setToolTip("CPU Governor file its not available - Governor selection disabled") + gov_layout.addWidget(gov_label) gov_layout.addWidget(widgets['gov_combo']) scroll_layout.addLayout(gov_layout) @@ -118,9 +178,75 @@ def create_cpu_tab(): widgets['current_gov_value'] = QLabel("Updating...") widgets['current_gov_value'].setContentsMargins(0, 0, 0, 10) scroll_layout.addWidget(widgets['current_gov_value']) + + min_freq_mhz = CPUManager.get_cpuinfo_min_freq() + max_freq_mhz = CPUManager.get_cpuinfo_max_freq() + freq_info_accessible = min_freq_mhz is not None and max_freq_mhz is not None + + max_freq_layout = QHBoxLayout() + max_freq_label = QLabel("Max Frequency (MHz):") + max_freq_label.setWordWrap(True) + max_freq_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + + widgets['max_freq_combo'] = QComboBox() + if freq_info_accessible: + freq_range = [str(f) for f in range(min_freq_mhz, max_freq_mhz + 100, 100)] + widgets['max_freq_combo'].addItems(["unset"] + list(reversed(freq_range))) + else: + widgets['max_freq_combo'].addItems(["unset"]) + + widgets['max_freq_combo'].setCurrentText("unset") + widgets['max_freq_combo'].setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + + max_freq_accessible = CPUManager.get_available_setting("/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq") + if not max_freq_accessible or not freq_info_accessible: + widgets['max_freq_combo'].setEnabled(False) + if not freq_info_accessible: + widgets['max_freq_combo'].setToolTip("CPU frequency info files not available - Max Frequency selection disabled") + else: + widgets['max_freq_combo'].setToolTip("CPU max frequency file its not available - Max Frequency selection disabled") + + max_freq_layout.addWidget(max_freq_label) + max_freq_layout.addWidget(widgets['max_freq_combo']) + scroll_layout.addLayout(max_freq_layout) + + widgets['current_max_freq_value'] = QLabel("Updating...") + widgets['current_max_freq_value'].setContentsMargins(0, 0, 0, 10) + scroll_layout.addWidget(widgets['current_max_freq_value']) + + min_freq_layout = QHBoxLayout() + min_freq_label = QLabel("Min Frequency (MHz):") + min_freq_label.setWordWrap(True) + min_freq_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + + widgets['min_freq_combo'] = QComboBox() + + if freq_info_accessible: + widgets['min_freq_combo'].addItems(["unset"] + freq_range) + else: + widgets['min_freq_combo'].addItems(["unset"]) + + widgets['min_freq_combo'].setCurrentText("unset") + widgets['min_freq_combo'].setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + + min_freq_accessible = CPUManager.get_available_setting("/sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq") + if not min_freq_accessible or not freq_info_accessible: + widgets['min_freq_combo'].setEnabled(False) + if not freq_info_accessible: + widgets['min_freq_combo'].setToolTip("CPU frequency info files not available - Min Frequency selection disabled") + else: + widgets['min_freq_combo'].setToolTip("CPU min frequency file its not available - Min Frequency selection disabled") + + min_freq_layout.addWidget(min_freq_label) + min_freq_layout.addWidget(widgets['min_freq_combo']) + scroll_layout.addLayout(min_freq_layout) + + widgets['current_min_freq_value'] = QLabel("Updating...") + widgets['current_min_freq_value'].setContentsMargins(0, 0, 0, 10) + scroll_layout.addWidget(widgets['current_min_freq_value']) sched_layout = QHBoxLayout() - sched_label = QLabel("Pluggable CPU Scheduler:") + sched_label = QLabel("Pluggable Scheduler:") sched_label.setWordWrap(True) sched_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) @@ -170,7 +296,7 @@ def create_cpu_apply_button(parent_layout, widgets): button_container = QWidget() button_container.setProperty("buttonContainer", True) button_layout = QHBoxLayout(button_container) - button_layout.setContentsMargins(11, 10, 11, 9) + button_layout.setContentsMargins(11, 10, 11, 0) widgets['cpu_apply_button'] = QPushButton("Apply") widgets['cpu_apply_button'].setMinimumSize(100, 30) @@ -181,18 +307,53 @@ def create_cpu_apply_button(parent_layout, widgets): button_layout.addStretch(1) parent_layout.addWidget(button_container) + parent_layout.addSpacing(9) @staticmethod def refresh_cpu_values(widgets): """ Updates the UI with current CPU governor and scheduler information. """ - widgets['current_gov_value'].setText("Updating...") - try: - current_governor = CPUManager.get_current_governor() - widgets['current_gov_value'].setText(f"current: {current_governor}") - except Exception: - widgets['current_gov_value'].setText("Error") + if not CPUManager.get_available_setting("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor"): + widgets['current_gov_value'].setText("current: unset") + else: + widgets['current_gov_value'].setText("Updating...") + try: + current_governor = CPUManager.get_current_governor() + if current_governor is not None: + widgets['current_gov_value'].setText(f"current: {current_governor}") + else: + widgets['current_gov_value'].setText("current: Error reading") + except Exception: + widgets['current_gov_value'].setText("current: Error") + + if widgets.get('max_freq_combo'): + if not CPUManager.get_available_setting("/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq"): + widgets['current_max_freq_value'].setText("current: unset") + else: + widgets['current_max_freq_value'].setText("Updating...") + try: + current_max_freq = CPUManager.get_current_scaling_max_freq() + if current_max_freq is not None: + widgets['current_max_freq_value'].setText(f"current: {current_max_freq}") + else: + widgets['current_max_freq_value'].setText("current: Error reading") + except Exception: + widgets['current_max_freq_value'].setText("current: Error") + + if widgets.get('min_freq_combo'): + if not CPUManager.get_available_setting("/sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq"): + widgets['current_min_freq_value'].setText("current: unset") + else: + widgets['current_min_freq_value'].setText("Updating...") + try: + current_min_freq = CPUManager.get_current_scaling_min_freq() + if current_min_freq is not None: + widgets['current_min_freq_value'].setText(f"current: {current_min_freq}") + else: + widgets['current_min_freq_value'].setText("current: Error reading") + except Exception: + widgets['current_min_freq_value'].setText("current: Error") widgets['current_sched_value'].setText("Updating...") try: @@ -211,7 +372,10 @@ def refresh_cpu_values(widgets): widgets['sched_combo'].addItems(widgets['available_schedulers']) widgets['sched_combo'].setCurrentText(current_selection) - widgets['current_sched_value'].setText(f"current: {running_scheduler}") + if running_scheduler is not None: + widgets['current_sched_value'].setText(f"current: {running_scheduler}") + else: + widgets['current_sched_value'].setText("current: Error reading") except Exception: - widgets['current_sched_value'].setText("Error") \ No newline at end of file + widgets['current_sched_value'].setText("current: Error") \ No newline at end of file diff --git a/src/kernel.py b/src/kernel.py index 8a39389..9c24507 100644 --- a/src/kernel.py +++ b/src/kernel.py @@ -11,102 +11,302 @@ class KernelManager: KERNEL_SETTINGS = { 'compaction_proactiveness': { 'path': '/proc/sys/vm/compaction_proactiveness', - 'text': 'Controls memory compaction proactiveness. Lower values reduce CPU overhead.\nRecommended values: 0', + 'text': 'Controls memory compaction aggressiveness (0-100). Lower values reduce CPU overhead during intensive workloads.\nRecommended: 0', 'is_dynamic': False }, 'watermark_boost_factor': { 'path': '/proc/sys/vm/watermark_boost_factor', - 'text': 'Controls memory reclaim aggressiveness. Lower values prevent excessive reclaim.\nRecommended values: 1', + 'text': 'Memory reclaim aggressiveness during fragmentation (units of 10,000). Lower values prevent background reclaim during high-load scenarios.\nRecommended: 0', + 'is_dynamic': False + }, + 'watermark_scale_factor': { + 'path': '/proc/sys/vm/watermark_scale_factor', + 'text': 'Controls kswapd aggressiveness (units of 10,000). Higher values mean more free memory maintained. Default is 10 (0.1% of memory).\nRecommended: 10 (default) or higher for latency-sensitive workloads', + 'is_dynamic': False + }, + 'extfrag_threshold': { + 'path': '/proc/sys/vm/extfrag_threshold', + 'text': 'External fragmentation threshold that triggers compaction (0-1000). Higher values reduce compaction overhead.\nRecommended: 500', + 'is_dynamic': False + }, + 'compact_unevictable_allowed': { + 'path': '/proc/sys/vm/compact_unevictable_allowed', + 'text': 'Allow compaction to examine unevictable (mlocked) pages. May cause minor page faults but improves compaction effectiveness.\nRecommended: 1 (default), 0 for RT systems', + 'is_dynamic': False + }, + 'defrag_mode': { + 'path': '/proc/sys/vm/defrag_mode', + 'text': 'Proactive fragmentation prevention for hugepage allocations. Reduces long-term fragmentation at cost of immediate overhead.\nRecommended: 0(less overhead), 1 (for long-running systems)', 'is_dynamic': False }, 'min_free_kbytes': { 'path': '/proc/sys/vm/min_free_kbytes', - 'text': 'Minimum free memory to maintain. Do not set below 1024 KB or above 5% of system memory.\nRecommended values: 1024-...', + 'text': 'Minimum reserved memory. Do not set below 1024 KB or above 5% of system memory.\nRecommended values: 1024-...', + 'is_dynamic': False + }, + 'overcommit_memory': { + 'path': '/proc/sys/vm/overcommit_memory', + 'text': 'Memory overcommit policy: 0=heuristic, 1=always, 2=strict. Mode 1 maximizes available memory.\nRecommended: 1', + 'is_dynamic': False + }, + 'overcommit_ratio': { + 'path': '/proc/sys/vm/overcommit_ratio', + 'text': 'Percentage of physical RAM (plus swap) available when overcommit_memory=2.\nRecommended: 60', + 'is_dynamic': False + }, + 'admin_reserve_kbytes': { + 'path': '/proc/sys/vm/admin_reserve_kbytes', + 'text': 'Memory reserved for root processes during OOM conditions. Default: min(3% of RAM, 8MB).\nRecommended: 8192 (8GB+ RAM), 4096 (4GB RAM)', + 'is_dynamic': False + }, + 'user_reserve_kbytes': { + 'path': '/proc/sys/vm/user_reserve_kbytes', + 'text': 'Memory reserved for user processes when overcommit_memory=2. Default: min(3% of process size, 128MB).\nRecommended: 131072', 'is_dynamic': False }, 'max_map_count': { 'path': '/proc/sys/vm/max_map_count', - 'text': 'Maximum number of memory map areas a process can have. For performance and compatibility.\nRecommended values: 1048576', + 'text': 'Maximum memory mappings per process. Essential for applications using many shared libraries and memory-mapped files.\nRecommended: 2147483642 (SteamDeck Value) or 1048576 (Arch Linux)', + 'is_dynamic': False + }, + 'mmap_min_addr': { + 'path': '/proc/sys/vm/mmap_min_addr', + 'text': 'Minimum virtual address for mmap operations. Security feature with minimal performance impact.\nRecommended: 65536', + 'is_dynamic': False + }, + 'page_lock_unfairness': { + 'path': '/proc/sys/vm/page_lock_unfairness', + 'text': 'Number of times page lock can be stolen from waiter before fair handoff. Higher values favor readers which can improve read performance for asset streaming workloads.\nRecommended: 5', 'is_dynamic': False }, 'swappiness': { 'path': '/proc/sys/vm/swappiness', - 'text': 'Controls how aggressively the kernel swaps memory pages. Lower values prefer RAM over swap.\nRecommended values: 10', + 'text': 'Kernel preference for swap vs RAM reclaim (0-200). Lower values prioritize keeping data in RAM for latency-sensitive workloads.\nRecommended: 10 (16GB+ RAM), 30-60 (8GB RAM)', + 'is_dynamic': False + }, + 'page-cluster': { + 'path': '/proc/sys/vm/page-cluster', + 'text': 'Number of pages to read/write together during swap operations as log2. Set to 0 for SSD-based systems.\nRecommended: 0', + 'is_dynamic': False + }, + 'vfs_cache_pressure': { + 'path': '/proc/sys/vm/vfs_cache_pressure', + 'text': 'Tendency to reclaim filesystem caches relative to pagecache/swap. Lower values improve asset loading performance by keeping metadata cached.\nRecommended: 50', + 'is_dynamic': False + }, + 'percpu_pagelist_high_fraction': { + 'path': '/proc/sys/vm/percpu_pagelist_high_fraction', + 'text': 'Per-CPU page list size as fraction of zone size (0=default kernel algorithm, min value is 8). Higher values reduce contention on multi-core systems.\nRecommended: 8+ or 0 (reverts to the default behavior)', + 'is_dynamic': False + }, + 'zone_reclaim_mode': { + 'path': '/proc/sys/vm/zone_reclaim_mode', + 'text': 'NUMA memory reclaim behavior (bitmask: 0=reclaim off 1=reclaim on, 2=write dirty pages, 4=swap pages). Usually degrades performance due to unnecessary reclaim overhead.\nRecommended: 0', + 'is_dynamic': False + }, + 'min_unmapped_ratio': { + 'path': '/proc/sys/vm/min_unmapped_ratio', + 'text': 'Minimum percentage of unmapped pages before zone reclaim (NUMA only). Higher values delay local reclaim, improving cache locality.\nRecommended: 1', + 'is_dynamic': False + }, + 'min_slab_ratio': { + 'path': '/proc/sys/vm/min_slab_ratio', + 'text': 'Percentage of zone pages that must be reclaimable slab before zone reclaim (NUMA only). Higher values delay expensive remote allocation.\nRecommended: 5 (default), 3-8 range for tuning', + 'is_dynamic': False + }, + 'numa_stat': { + 'path': '/proc/sys/vm/numa_stat', + 'text': 'Enable NUMA statistics collection. Disabling reduces allocation overhead but breaks monitoring tools.\nRecommended: 0 (performance), 1 (monitoring/debugging)', + 'is_dynamic': False + }, + 'oom_kill_allocating_task': { + 'path': '/proc/sys/vm/oom_kill_allocating_task', + 'text': 'OOM killer targets the task that triggered OOM instead of scanning all tasks. Improves recovery speed.\nRecommended: 1', + 'is_dynamic': False + }, + 'oom_dump_tasks': { + 'path': '/proc/sys/vm/oom_dump_tasks', + 'text': 'Enable task dump when OOM killer is invoked. Useful for debugging but adds overhead on large systems.\nRecommended: 0 (disable for performance), 1 (enable for debugging)', + 'is_dynamic': False + }, + 'panic_on_oom': { + 'path': '/proc/sys/vm/panic_on_oom', + 'text': 'System behavior on OOM: 0=kill process, 1=panic on system OOM, 2=always panic.\nRecommended: 0 (default behavior)', 'is_dynamic': False }, 'dirty_ratio': { 'path': '/proc/sys/vm/dirty_ratio', - 'text': 'Percentage of system memory that can be filled with dirty pages before processes are forced to write.\nRecommended values: 15-20', + 'text': 'Maximum percentage of available memory for dirty pages before synchronous writes. Lower values reduce I/O latency spikes. Mutually exclusive with dirty_bytes.\nRecommended: 10', 'is_dynamic': False }, 'dirty_background_ratio': { 'path': '/proc/sys/vm/dirty_background_ratio', - 'text': 'Percentage of system memory at which background writeback starts.\nRecommended values: 5-10', + 'text': 'Percentage of available memory at which background writeback begins. Should be 1/3 of dirty_ratio. Mutually exclusive with dirty_background_bytes.\nRecommended: 3', + 'is_dynamic': False + }, + 'dirty_bytes': { + 'path': '/proc/sys/vm/dirty_bytes', + 'text': 'Absolute dirty memory limit (bytes). Provides consistent behavior regardless of RAM size. Mutually exclusive with dirty_ratio.\nRecommended: 67108864 (64MB)', + 'is_dynamic': False + }, + 'dirty_background_bytes': { + 'path': '/proc/sys/vm/dirty_background_bytes', + 'text': 'Absolute background writeback threshold (bytes). Should be 50% of dirty_bytes. Mutually exclusive with dirty_background_ratio.\nRecommended: 33554432 (32MB)', 'is_dynamic': False }, 'dirty_expire_centisecs': { 'path': '/proc/sys/vm/dirty_expire_centisecs', - 'text': 'How long dirty data can remain in memory before being written (in centiseconds).\nRecommended values: 1500-3000', + 'text': 'Maximum time dirty data remains in memory (centiseconds). Shorter intervals improve responsiveness.\nRecommended: 3000', 'is_dynamic': False }, 'dirty_writeback_centisecs': { 'path': '/proc/sys/vm/dirty_writeback_centisecs', - 'text': 'Interval between periodic writeback wakeups (in centiseconds).\nRecommended values: 500-1500', + 'text': 'Interval between periodic writeback wakeups (centiseconds). Longer intervals reduce CPU overhead.\nRecommended: 1500', 'is_dynamic': False }, - 'vfs_cache_pressure': { - 'path': '/proc/sys/vm/vfs_cache_pressure', - 'text': 'Controls tendency of kernel to reclaim directory and inode cache memory. Lower values keep caches longer.\nRecommended values: 50-80', + 'dirtytime_expire_seconds': { + 'path': '/proc/sys/vm/dirtytime_expire_seconds', + 'text': 'Interval for lazy timestamp updates on filesystems with dirtytime mount option (seconds).\nRecommended: 43200 (12 hours)', + 'is_dynamic': False + }, + 'laptop_mode': { + 'path': '/proc/sys/vm/laptop_mode', + 'text': 'Power-saving write delay mechanism. Disable for performance-oriented systems.\nRecommended: 0', + 'is_dynamic': False + }, + 'nr_hugepages': { + 'path': '/proc/sys/vm/nr_hugepages', + 'text': 'Number of persistent hugepages allocated. Critical for applications requiring guaranteed hugepage memory (databases, VMs, HPC).\nRecommended: 0 (default), or calculated based on application needs', + 'is_dynamic': False + }, + 'nr_overcommit_hugepages': { + 'path': '/proc/sys/vm/nr_overcommit_hugepages', + 'text': 'Maximum number of additional hugepages that can be allocated dynamically beyond nr_hugepages.\nRecommended: 0 (conservative), or set based on peak demand', + 'is_dynamic': False + }, + 'hugetlb_optimize_vmemmap': { + 'path': '/proc/sys/vm/hugetlb_optimize_vmemmap', + 'text': 'Optimize hugepage metadata memory usage (saves ~7 pages per 2MB hugepage). May add overhead during allocation/deallocation.\nRecommended: 1 (enable for memory savings), 0 (disable for allocation speed)', + 'is_dynamic': False + }, + 'stat_interval': { + 'path': '/proc/sys/vm/stat_interval', + 'text': 'VM statistics update interval (seconds). Higher values reduce CPU overhead.\nRecommended: 10', 'is_dynamic': False }, 'thp_enabled': { 'path': '/sys/kernel/mm/transparent_hugepage/enabled', - 'text': 'Controls transparent huge pages. Can improve performance but may increase memory usage.', + 'text': 'Transparent Huge Pages reduce TLB pressure but may cause allocation stalls. "madvise" enables only where beneficial.\nRecommended: madvise', 'is_dynamic': True }, 'thp_shmem_enabled': { 'path': '/sys/kernel/mm/transparent_hugepage/shmem_enabled', - 'text': 'Controls transparent huge pages for shared memory. May improve performance for memory-intensive applications.', + 'text': 'THP for shared memory segments. "advise" enables only when explicitly requested.\nRecommended: advise', 'is_dynamic': True }, 'thp_defrag': { 'path': '/sys/kernel/mm/transparent_hugepage/defrag', - 'text': 'Controls when kernel attempts to make huge pages available through memory compaction.', + 'text': 'THP defragmentation strategy. "defer" prevents allocation stalls during high-priority tasks.\nRecommended: defer', 'is_dynamic': True }, - 'zone_reclaim_mode': { - 'path': '/proc/sys/vm/zone_reclaim_mode', - 'text': 'Controls zone reclaim behavior in NUMA systems. Disabling improves performance on most systems.\nRecommended values: 0', + 'max_user_freq': { + 'path': '/sys/class/rtc/rtc0/max_user_freq', + 'text': 'Maximum RTC interrupt frequency (Hz) for userspace. Higher values provide better timer precision.\nRecommended: 64', 'is_dynamic': False }, - 'page_lock_unfairness': { - 'path': '/proc/sys/vm/page_lock_unfairness', - 'text': 'Controls page lock unfairness to prevent lock starvation.\nRecommended values: 1', + 'numa_balancing': { + 'path': '/proc/sys/kernel/numa_balancing', + 'text': 'Automatic NUMA memory migration. Creates overhead without significant benefits for most workloads.\nRecommended: 0', + 'is_dynamic': False + }, + 'randomize_va_space': { + 'path': '/proc/sys/kernel/randomize_va_space', + 'text': 'Address space layout randomization: 0=disabled, 1=conservative, 2=full. Lower values reduce address translation overhead.\nRecommended: 0 (performance), 2 (security)', + 'is_dynamic': False + }, + 'perf_event_paranoid': { + 'path': '/proc/sys/kernel/perf_event_paranoid', + 'text': 'Performance monitoring access: -1=unrestricted, 0=user+kernel, 1=user only, 2=kernel only, 3=no access.\nRecommended: 2', 'is_dynamic': False }, 'sched_cfs_bandwidth_slice_us': { 'path': '/proc/sys/kernel/sched_cfs_bandwidth_slice_us', - 'text': 'CFS bandwidth slice duration in microseconds. Affects scheduler responsiveness.\nRecommended values: 3000', + 'text': 'CFS bandwidth slice duration (microseconds). Higher values reduce scheduler overhead for CPU-bound applications.\nRecommended: 4000', 'is_dynamic': False }, 'sched_autogroup_enabled': { 'path': '/proc/sys/kernel/sched_autogroup_enabled', - 'text': 'Enables automatic process grouping for better desktop responsiveness.\nRecommended values: 1', + 'text': 'Automatic process grouping for desktop responsiveness. Helps prioritize foreground applications.\nRecommended: 1 or 0 to use nice', + 'is_dynamic': False + }, + 'sched_rt_runtime_us': { + 'path': '/proc/sys/kernel/sched_rt_runtime_us', + 'text': 'Maximum CPU time (microseconds) for realtime tasks per period. -1 allows unlimited usage.\nRecommended: 950000 or -1', + 'is_dynamic': False + }, + 'sched_rt_period_us': { + 'path': '/proc/sys/kernel/sched_rt_period_us', + 'text': 'Period over which realtime task CPU usage is measured (microseconds). Works with sched_rt_runtime_us.\nRecommended: 1000000 (1 second, default)', + 'is_dynamic': False + }, + 'sched_schedstats': { + 'path': '/proc/sys/kernel/sched_schedstats', + 'text': 'Scheduler statistics collection. Disable to eliminate tracing overhead.\nRecommended: 0', + 'is_dynamic': False + }, + 'timer_migration': { + 'path': '/proc/sys/kernel/timer_migration', + 'text': 'Allows timer interrupts to migrate between CPUs. Disabling reduces latency at cost of power efficiency.\nRecommended: 0', 'is_dynamic': False }, 'watchdog': { 'path': '/proc/sys/kernel/watchdog', - 'text': 'Enables soft lockup detector. \nRecommended values: 0', + 'text': 'Soft lockup detector. Disable to remove periodic checks that can cause stuttering.\nRecommended: 0', 'is_dynamic': False }, 'nmi_watchdog': { 'path': '/proc/sys/kernel/nmi_watchdog', - 'text': 'Enables NMI watchdog for hard lockup detection. \nRecommended values: 0', + 'text': 'NMI-based hard lockup detection. Disables performance-counter based monitoring.\nRecommended: 0', 'is_dynamic': False }, - 'laptop_mode': { - 'path': '/proc/sys/vm/laptop_mode', - 'text': 'Enables laptop power-saving mode for disk I/O. \nRecommended values: 0', + 'hung_task_timeout_secs': { + 'path': '/proc/sys/kernel/hung_task_timeout_secs', + 'text': 'Timeout for detecting hung tasks (seconds). 0 disables detection to prevent false positives during long sessions.\nRecommended: 0 or 120 (Default)', + 'is_dynamic': False + }, + 'pid_max': { + 'path': '/proc/sys/kernel/pid_max', + 'text': 'Maximum process ID value. Higher values support systems running many concurrent processes.\nRecommended: 4194304', + 'is_dynamic': False + }, + 'file_max': { + 'path': '/proc/sys/fs/file-max', + 'text': 'System-wide maximum open file descriptors. Essential for applications opening many files simultaneously.\nRecommended: 2097152', + 'is_dynamic': False + }, + 'core_rmem_max': { + 'path': '/proc/sys/net/core/rmem_max', + 'text': 'Maximum socket receive buffer size (bytes) per socket. Higher values improve throughput for high-bandwidth connections.\nRecommended: 268435456 (256MB)', + 'is_dynamic': False + }, + 'core_wmem_max': { + 'path': '/proc/sys/net/core/wmem_max', + 'text': 'Maximum socket send buffer size (bytes) per socket. Should match rmem_max for balanced performance.\nRecommended: 268435456 (256MB)', + 'is_dynamic': False + }, + 'tcp_fastopen': { + 'path': '/proc/sys/net/ipv4/tcp_fastopen', + 'text': 'TCP Fast Open reduces connection establishment latency. Bitmask: 1=client, 2=server.\nRecommended: 3 (enable for both incoming/outgoing)', + 'is_dynamic': False + }, + 'tcp_window_scaling': { + 'path': '/proc/sys/net/ipv4/tcp_window_scaling', + 'text': 'Enable TCP window scaling for high-bandwidth connections.\nRecommended: 1', + 'is_dynamic': False + }, + 'tcp_timestamps': { + 'path': '/proc/sys/net/ipv4/tcp_timestamps', + 'text': 'Enable TCP timestamps for RTT measurement and PAWS protection.\nRecommended: 1', 'is_dynamic': False } } @@ -120,7 +320,6 @@ def get_current_value(setting_path): with open(setting_path, 'r') as f: return f.read().strip() except Exception as e: - print(f"Error reading kernel setting {setting_path}: {e}") return None @staticmethod @@ -140,10 +339,8 @@ def get_dynamic_current_value(setting_path): if values: return values[0] else: - print(f"Warning: No values found in dynamic setting {setting_path}") return None except Exception as e: - print(f"Error reading dynamic kernel setting {setting_path}: {e}") return None @staticmethod @@ -162,10 +359,8 @@ def get_dynamic_possible_values(setting_path): if possible_values: return possible_values else: - print(f"Warning: No possible values found in dynamic setting {setting_path}") return None except Exception as e: - print(f"Error reading possible values for {setting_path}: {e}") return None @staticmethod @@ -264,7 +459,7 @@ def create_setting_section(kernel_layout, widgets, setting_name, setting_info): if not is_accessible: input_widget.setEnabled(False) - input_widget.setToolTip(f"Kernel setting not avaliable - {setting_name} disabled") + input_widget.setToolTip(f"Setting file its not available - {setting_name} disabled") widgets[f'{setting_name}_input'] = input_widget widgets[f'{setting_name}_current_value'] = current_value_label diff --git a/src/options.py b/src/options.py index 7d201a3..1d2b543 100644 --- a/src/options.py +++ b/src/options.py @@ -11,73 +11,15 @@ class OptionsManager: Manages application settings and preferences with integrated UI. """ - def __init__(self, parent, main_window): - self.parent = parent - self.main_window = main_window - self.options_path = Path(os.path.expanduser("~/.config/volt-gui/volt-options.ini")) - self.options_path.parent.mkdir(parents=True, exist_ok=True) - self.widgets = {} - self.widget = QWidget(self.parent) - self.setup_ui() - - def get_widget(self): - """Get the widget for adding to tab widget.""" - return self.widget - - def setup_ui(self): + @staticmethod + def create_options_tab(main_window): """ - Set up the user interface for the options tab. + Creates and returns the options management tab widget. """ - main_layout = QVBoxLayout(self.widget) + options_tab = QWidget() + main_layout = QVBoxLayout(options_tab) main_layout.setContentsMargins(9, 0, 9, 0) - main_layout.setSpacing(10) - - scroll_area = self.create_scroll_area() - main_layout.addWidget(scroll_area) - - self.apply_button = self.create_option_apply_button(main_layout) - - self.register_widgets() - self.apply_button.clicked.connect(self.save_and_apply_options) - self.load_options() - - def create_option_apply_button(self, parent_layout): - """ - Create and setup the apply button for options. - """ - button_container = QWidget() - button_container.setProperty("buttonContainer", True) - button_layout = QHBoxLayout(button_container) - button_layout.setContentsMargins(11, 10, 11, 0) - - apply_button = QPushButton("Apply") - apply_button.setMinimumSize(100, 30) - apply_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - button_layout.addStretch(1) - button_layout.addWidget(apply_button) - button_layout.addStretch(1) - - parent_layout.addWidget(button_container) - parent_layout.addSpacing(9) - return apply_button - - def save_and_apply_options(self): - """ - Save current options and apply them to the application. - """ - self.save_options() - - if self.main_window and hasattr(self.main_window, 'tray_icon'): - self.main_window.tray_icon.showMessage("volt-gui", "Options saved successfully", self.main_window.tray_icon.MessageIcon.Information, 2000) - else: - QMessageBox.information(self.main_window, "volt-gui", "Options saved successfully") - - def create_scroll_area(self): - """ - Create and configure the scroll area with all option widgets. - """ scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) @@ -88,288 +30,310 @@ def create_scroll_area(self): scroll_layout.setSpacing(10) scroll_layout.setContentsMargins(10, 10, 10, 0) - self.add_theme_option(scroll_layout) - self.add_transparency_option(scroll_layout) - self.add_tray_option(scroll_layout) - self.add_start_minimized_option(scroll_layout) - self.add_start_maximized_option(scroll_layout) - self.add_welcome_message_option(scroll_layout) + widgets = {} - scroll_layout.addStretch(1) - scroll_area.setWidget(scroll_widget) - return scroll_area - - def add_theme_option(self, layout): - """ - Add theme selection option to layout. - """ theme_layout = QHBoxLayout() theme_label = QLabel("Selected Theme:") theme_label.setWordWrap(True) theme_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - self.theme_combo = QComboBox() - self.theme_combo.addItems(["amd", "intel", "nvidia"]) - self.theme_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + widgets['theme_combo'] = QComboBox() + widgets['theme_combo'].addItems(["amd", "intel", "nvidia"]) + widgets['theme_combo'].setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) theme_layout.addWidget(theme_label) - theme_layout.addWidget(self.theme_combo) - layout.addLayout(theme_layout) + theme_layout.addWidget(widgets['theme_combo']) + scroll_layout.addLayout(theme_layout) - def add_transparency_option(self, layout): - """ - Add transparency option to layout. - """ transparency_layout = QHBoxLayout() transparency_label = QLabel("Transparency:") transparency_label.setWordWrap(True) transparency_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - self.transparency_combo = QComboBox() - self.transparency_combo.addItems(["enable", "disable"]) - self.transparency_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + widgets['transparency_combo'] = QComboBox() + widgets['transparency_combo'].addItems(["enable", "disable"]) + widgets['transparency_combo'].setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) transparency_layout.addWidget(transparency_label) - transparency_layout.addWidget(self.transparency_combo) - layout.addLayout(transparency_layout) + transparency_layout.addWidget(widgets['transparency_combo']) + scroll_layout.addLayout(transparency_layout) - def add_tray_option(self, layout): - """ - Add system tray option to layout. - """ tray_layout = QHBoxLayout() tray_label = QLabel("Run in System Tray:") tray_label.setWordWrap(True) tray_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - self.tray_combo = QComboBox() - self.tray_combo.addItems(["enable", "disable"]) - self.tray_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + widgets['tray_combo'] = QComboBox() + widgets['tray_combo'].addItems(["enable", "disable"]) + widgets['tray_combo'].setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) tray_layout.addWidget(tray_label) - tray_layout.addWidget(self.tray_combo) - layout.addLayout(tray_layout) + tray_layout.addWidget(widgets['tray_combo']) + scroll_layout.addLayout(tray_layout) - def add_start_minimized_option(self, layout): - """ - Add start minimized option to layout. - """ start_minimized_layout = QHBoxLayout() start_minimized_label = QLabel("Open Minimized:") start_minimized_label.setWordWrap(True) start_minimized_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - self.start_minimized_combo = QComboBox() - self.start_minimized_combo.addItems(["enable", "disable"]) - self.start_minimized_combo.setCurrentText("disable") - self.start_minimized_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + widgets['start_minimized_combo'] = QComboBox() + widgets['start_minimized_combo'].addItems(["enable", "disable"]) + widgets['start_minimized_combo'].setCurrentText("disable") + widgets['start_minimized_combo'].setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) start_minimized_layout.addWidget(start_minimized_label) - start_minimized_layout.addWidget(self.start_minimized_combo) - layout.addLayout(start_minimized_layout) + start_minimized_layout.addWidget(widgets['start_minimized_combo']) + scroll_layout.addLayout(start_minimized_layout) - def add_start_maximized_option(self, layout): - """ - Add start maximized option to layout. - """ start_maximized_layout = QHBoxLayout() start_maximized_label = QLabel("Open Maximized:") start_maximized_label.setWordWrap(True) start_maximized_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - self.start_maximized_combo = QComboBox() - self.start_maximized_combo.addItems(["enable", "disable"]) - self.start_maximized_combo.setCurrentText("disable") - self.start_maximized_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + widgets['start_maximized_combo'] = QComboBox() + widgets['start_maximized_combo'].addItems(["enable", "disable"]) + widgets['start_maximized_combo'].setCurrentText("disable") + widgets['start_maximized_combo'].setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) start_maximized_layout.addWidget(start_maximized_label) - start_maximized_layout.addWidget(self.start_maximized_combo) - layout.addLayout(start_maximized_layout) + start_maximized_layout.addWidget(widgets['start_maximized_combo']) + scroll_layout.addLayout(start_maximized_layout) - def add_welcome_message_option(self, layout): - """ - Add welcome message option to layout. - """ welcome_message_layout = QHBoxLayout() welcome_message_label = QLabel("Welcome Message:") welcome_message_label.setWordWrap(True) welcome_message_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) - self.welcome_message_combo = QComboBox() - self.welcome_message_combo.addItems(["enable", "disable"]) - self.welcome_message_combo.setCurrentText("enable") - self.welcome_message_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + widgets['welcome_message_combo'] = QComboBox() + widgets['welcome_message_combo'].addItems(["enable", "disable"]) + widgets['welcome_message_combo'].setCurrentText("enable") + widgets['welcome_message_combo'].setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) welcome_message_layout.addWidget(welcome_message_label) - welcome_message_layout.addWidget(self.welcome_message_combo) - layout.addLayout(welcome_message_layout) + welcome_message_layout.addWidget(widgets['welcome_message_combo']) + scroll_layout.addLayout(welcome_message_layout) + + scroll_layout.addStretch(1) + scroll_area.setWidget(scroll_widget) + main_layout.addWidget(scroll_area) + + OptionsManager.create_option_apply_button(main_layout, widgets, main_window) + + widgets['main_window'] = main_window + widgets['options_path'] = Path(os.path.expanduser("~/.config/volt-gui/volt-options.ini")) + widgets['options_path'].parent.mkdir(parents=True, exist_ok=True) + + OptionsManager.set_default_values(widgets) + + return options_tab, widgets - def register_widgets(self): + @staticmethod + def create_option_apply_button(parent_layout, widgets, main_window): """ - Register all widgets with the options manager. + Creates and adds the options apply button to the layout. """ - self.widgets = { - 'theme_combo': self.theme_combo, - 'transparency_combo': self.transparency_combo, - 'tray_combo': self.tray_combo, - 'start_minimized_combo': self.start_minimized_combo, - 'start_maximized_combo': self.start_maximized_combo, - 'welcome_message_combo': self.welcome_message_combo, - 'apply_button': self.apply_button - } + button_container = QWidget() + button_container.setProperty("buttonContainer", True) + button_layout = QHBoxLayout(button_container) + button_layout.setContentsMargins(11, 10, 11, 0) + + widgets['options_apply_button'] = QPushButton("Apply") + widgets['options_apply_button'].setMinimumSize(100, 30) + widgets['options_apply_button'].setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - def load_options(self): + button_layout.addStretch(1) + button_layout.addWidget(widgets['options_apply_button']) + button_layout.addStretch(1) + + parent_layout.addWidget(button_container) + parent_layout.addSpacing(9) + + @staticmethod + def set_default_values(widgets): + """ + Set default values for all widgets. + """ + widgets['theme_combo'].setCurrentText("amd") + widgets['tray_combo'].setCurrentText("disable") + widgets['transparency_combo'].setCurrentText("disable") + widgets['start_minimized_combo'].setCurrentText("disable") + widgets['start_maximized_combo'].setCurrentText("disable") + widgets['welcome_message_combo'].setCurrentText("enable") + + @staticmethod + def load_options(widgets): """ Load options from the configuration file. """ - self.set_default_values() + OptionsManager.set_default_values(widgets) - if not self.options_path.exists(): - self.save_options() + if not widgets['options_path'].exists(): + OptionsManager.save_options(widgets) return options = configparser.ConfigParser() - options.read(self.options_path) + options.read(widgets['options_path']) - self.apply_options_values(options) - self.apply_all_options() + OptionsManager.apply_options_values(options, widgets) + OptionsManager.apply_all_options(widgets) - def save_options(self): + @staticmethod + def save_options(widgets): """ Save current options to the configuration file. """ options = configparser.ConfigParser() - options['Theme'] = {'ActiveTheme': self.widgets['theme_combo'].currentText()} - options['SystemTray'] = {'Enable': self.widgets['tray_combo'].currentText()} - options['Transparency'] = {'Enable': self.widgets['transparency_combo'].currentText()} - options['StartupMinimized'] = {'Enable': self.widgets['start_minimized_combo'].currentText()} - options['StartupMaximized'] = {'Enable': self.widgets['start_maximized_combo'].currentText()} - options['WelcomeMessage'] = {'Show': self.widgets['welcome_message_combo'].currentText()} - options['Profile'] = {'LastActiveProfile': getattr(self.main_window, 'current_profile', 'Default')} + main_window = widgets['main_window'] + + options['Theme'] = {'ActiveTheme': widgets['theme_combo'].currentText()} + options['SystemTray'] = {'Enable': widgets['tray_combo'].currentText()} + options['Transparency'] = {'Enable': widgets['transparency_combo'].currentText()} + options['StartupMinimized'] = {'Enable': widgets['start_minimized_combo'].currentText()} + options['StartupMaximized'] = {'Enable': widgets['start_maximized_combo'].currentText()} + options['WelcomeMessage'] = {'Show': widgets['welcome_message_combo'].currentText()} + options['Profile'] = {'LastActiveProfile': getattr(main_window, 'current_profile', 'Default')} - os.makedirs(os.path.dirname(self.options_path), exist_ok=True) + os.makedirs(os.path.dirname(widgets['options_path']), exist_ok=True) - with open(self.options_path, 'w') as optionsfile: + with open(widgets['options_path'], 'w') as optionsfile: options.write(optionsfile) - if self.main_window: - self.apply_all_options() + OptionsManager.apply_all_options(widgets) - def set_default_values(self): - """ - Set default values for all widgets. - """ - self.widgets['theme_combo'].setCurrentText("amd") - self.widgets['tray_combo'].setCurrentText("enable") - self.widgets['transparency_combo'].setCurrentText("enable") - self.widgets['start_minimized_combo'].setCurrentText("disable") - self.widgets['start_maximized_combo'].setCurrentText("disable") - self.widgets['welcome_message_combo'].setCurrentText("enable") - - def apply_options_values(self, options): + @staticmethod + def apply_options_values(options, widgets): """ Apply values from options file to widgets. """ - self.widgets['theme_combo'].setCurrentText(options.get('Theme', 'ActiveTheme', fallback="amd")) - self.widgets['tray_combo'].setCurrentText(options.get('SystemTray', 'Enable', fallback="enable")) - self.widgets['transparency_combo'].setCurrentText(options.get('Transparency', 'Enable', fallback="enable")) - self.widgets['start_minimized_combo'].setCurrentText(options.get('StartupMinimized', 'Enable', fallback="disable")) - self.widgets['start_maximized_combo'].setCurrentText(options.get('StartupMaximized', 'Enable', fallback="disable")) - self.widgets['welcome_message_combo'].setCurrentText(options.get('WelcomeMessage', 'Show', fallback="enable")) + main_window = widgets['main_window'] + + widgets['theme_combo'].setCurrentText(options.get('Theme', 'ActiveTheme', fallback="amd")) + widgets['tray_combo'].setCurrentText(options.get('SystemTray', 'Enable', fallback="disable")) + widgets['transparency_combo'].setCurrentText(options.get('Transparency', 'Enable', fallback="disable")) + widgets['start_minimized_combo'].setCurrentText(options.get('StartupMinimized', 'Enable', fallback="disable")) + widgets['start_maximized_combo'].setCurrentText(options.get('StartupMaximized', 'Enable', fallback="disable")) + widgets['welcome_message_combo'].setCurrentText(options.get('WelcomeMessage', 'Show', fallback="enable")) last_profile = options.get('Profile', 'LastActiveProfile', fallback='Default') - index = self.main_window.profile_selector.findText(last_profile) + index = main_window.profile_selector.findText(last_profile) if index >= 0: - self.main_window.profile_selector.setCurrentText(last_profile) - self.main_window.current_profile = last_profile + main_window.profile_selector.setCurrentText(last_profile) + main_window.current_profile = last_profile - def apply_all_options(self): + @staticmethod + def apply_all_options(widgets): """ Apply all options to the application. """ - self.apply_system_tray_options() - self.apply_transparency_options() - self.apply_theme_options() - self.apply_start_minimized_options() - self.apply_start_maximized_options() - self.apply_welcome_message_options() + OptionsManager.apply_system_tray_options(widgets) + OptionsManager.apply_transparency_options(widgets) + OptionsManager.apply_theme_options(widgets) + OptionsManager.apply_start_minimized_options(widgets) + OptionsManager.apply_start_maximized_options(widgets) + OptionsManager.apply_welcome_message_options(widgets) - def apply_theme_options(self): + @staticmethod + def apply_theme_options(widgets): """ Apply the selected theme to the application. """ - if self.main_window: - theme_name = self.widgets['theme_combo'].currentText() + main_window = widgets['main_window'] + if main_window: + theme_name = widgets['theme_combo'].currentText() ThemeManager.apply_theme(QApplication.instance(), theme_name) print(f"Theme option applied: {theme_name}") - def apply_system_tray_options(self): + @staticmethod + def apply_system_tray_options(widgets): """ Apply system tray options to the main window. """ - if self.main_window: - run_in_tray = self.widgets['tray_combo'].currentText() == 'enable' - old_option = self.main_window.use_system_tray - self.main_window.use_system_tray = run_in_tray + main_window = widgets['main_window'] + if main_window: + run_in_tray = widgets['tray_combo'].currentText() == 'enable' + old_option = main_window.use_system_tray + main_window.use_system_tray = run_in_tray if old_option != run_in_tray: if run_in_tray: - if not hasattr(self.main_window, 'tray_icon'): - self.main_window.setup_system_tray() + if not hasattr(main_window, 'tray_icon'): + main_window.setup_system_tray() else: - if hasattr(self.main_window, 'tray_icon'): - self.main_window.tray_icon.hide() - self.main_window.tray_icon.deleteLater() - delattr(self.main_window, 'tray_icon') - if not self.main_window.isVisible(): - self.main_window.show_and_activate() + if hasattr(main_window, 'tray_icon'): + main_window.tray_icon.hide() + main_window.tray_icon.deleteLater() + delattr(main_window, 'tray_icon') + if not main_window.isVisible(): + main_window.show_and_activate() - self.main_window.update_quit_behavior() + main_window.update_quit_behavior() print(f"System tray option applied: {run_in_tray}") - def apply_transparency_options(self): + @staticmethod + def apply_transparency_options(widgets): """ Apply window transparency options to the main window. """ - if self.main_window: - transparency_enabled = self.widgets['transparency_combo'].currentText() == 'enable' + main_window = widgets['main_window'] + if main_window: + transparency_enabled = widgets['transparency_combo'].currentText() == 'enable' if transparency_enabled: - self.main_window.setWindowOpacity(0.9) + main_window.setWindowOpacity(0.9) else: - self.main_window.setWindowOpacity(1.0) + main_window.setWindowOpacity(1.0) print(f"Transparency option applied: {transparency_enabled}") - def apply_start_minimized_options(self): + @staticmethod + def apply_start_minimized_options(widgets): """ Apply the start minimized option to the application. """ - if self.main_window: - start_minimized = self.widgets['start_minimized_combo'].currentText() == 'enable' - self.main_window.start_minimized = start_minimized + main_window = widgets['main_window'] + if main_window: + start_minimized = widgets['start_minimized_combo'].currentText() == 'enable' + main_window.start_minimized = start_minimized print(f"Start minimized option applied: {start_minimized}") - def apply_start_maximized_options(self): + @staticmethod + def apply_start_maximized_options(widgets): """ Apply the start maximized option to the application. """ - if self.main_window: - start_maximized = self.widgets['start_maximized_combo'].currentText() == 'enable' - self.main_window.start_maximized = start_maximized + main_window = widgets['main_window'] + if main_window: + start_maximized = widgets['start_maximized_combo'].currentText() == 'enable' + main_window.start_maximized = start_maximized print(f"Start maximized option applied: {start_maximized}") - def apply_welcome_message_options(self): + @staticmethod + def apply_welcome_message_options(widgets): """ Apply the welcome message option to the application. """ - if self.main_window: - show_welcome = self.widgets['welcome_message_combo'].currentText() == 'enable' - self.main_window.show_welcome = show_welcome + main_window = widgets['main_window'] + if main_window: + show_welcome = widgets['welcome_message_combo'].currentText() == 'enable' + main_window.show_welcome = show_welcome print(f"Welcome message option applied: {show_welcome}") - def get_welcome_message_setting(self): + @staticmethod + def get_welcome_message_setting(widgets): """ Get the current welcome message setting. """ - return self.widgets['welcome_message_combo'].currentText() == 'enable' \ No newline at end of file + return widgets['welcome_message_combo'].currentText() == 'enable' + + @staticmethod + def save_and_apply_options(widgets): + """ + Save current options and apply them to the application. + """ + main_window = widgets['main_window'] + OptionsManager.save_options(widgets) + + if main_window and hasattr(main_window, 'tray_icon'): + main_window.tray_icon.showMessage("volt-gui", "Options saved successfully", main_window.tray_icon.MessageIcon.Information, 2000) + else: + QMessageBox.information(main_window, "volt-gui", "Options saved successfully") \ No newline at end of file diff --git a/src/theme.py b/src/theme.py index 1d22596..cc09ba2 100644 --- a/src/theme.py +++ b/src/theme.py @@ -1,6 +1,4 @@ from PySide6.QtGui import QPalette, QColor -from PySide6.QtCore import Qt -from PySide6.QtWidgets import QApplication class ThemeManager: diff --git a/src/volt-gui.py b/src/volt-gui.py index ca89d7e..d816619 100644 --- a/src/volt-gui.py +++ b/src/volt-gui.py @@ -3,7 +3,6 @@ import signal import socket import threading -from pathlib import Path from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QPushButton, QSystemTrayIcon, QMenu, QMessageBox, QTabWidget, QFrame, QInputDialog from PySide6.QtCore import Qt, QProcess, Signal, QObject, QTimer from PySide6.QtGui import QIcon, QAction @@ -136,7 +135,7 @@ def __init__(self, instance_checker): """ super().__init__() self.instance_checker = instance_checker - self.use_system_tray = True + self.use_system_tray = False self.start_minimized = False self.start_maximized = False self.current_profile = "Default" @@ -146,9 +145,9 @@ def __init__(self, instance_checker): self.gpu_widgets = {} self.extras_widgets = {} self.about_widgets = {} + self.options_widgets = {} self.welcome_window = None - self.options_manager = None self.instance_checker.signals.show_window.connect(self.handle_show_window_signal) self.setWindowTitle("volt-gui") @@ -158,9 +157,6 @@ def __init__(self, instance_checker): self.apply_dark_theme() self.setup_ui() - if self.use_system_tray: - self.setup_system_tray() - self.update_quit_behavior() self.setup_refresh_timer() self.load_saved_settings() @@ -168,7 +164,7 @@ def __init__(self, instance_checker): self.setAttribute(Qt.WA_DontShowOnScreen, False) - if self.options_manager and self.options_manager.get_welcome_message_setting(): + if OptionsManager.get_welcome_message_setting(self.options_widgets): QTimer.singleShot(100, self.show_welcome_window) if not self.start_minimized: @@ -195,10 +191,7 @@ def setup_ui(self): self.setup_kernel_tab() self.setup_launch_options_tab() self.setup_extras_tab() - - self.options_manager = OptionsManager(self.tab_widget, self) - self.tab_widget.addTab(self.options_manager.get_widget(), "Options") - + self.setup_options_tab() self.setup_about_tab() main_layout.addWidget(self.tab_widget) @@ -315,6 +308,14 @@ def setup_extras_tab(self): extras_tab, self.extras_widgets = ExtrasManager.create_extras_tab() self.tab_widget.addTab(extras_tab, "Extras") + def setup_options_tab(self): + """ + Set up the options tab. + """ + options_tab, self.options_widgets = OptionsManager.create_options_tab(self) + self.options_widgets['options_apply_button'].clicked.connect(lambda: OptionsManager.save_and_apply_options(self.options_widgets)) + self.tab_widget.addTab(options_tab, "Options") + def setup_about_tab(self): """ Set up the about tab. @@ -445,6 +446,8 @@ def load_saved_settings(self): self.disk_widgets, self.current_profile ) + + OptionsManager.load_options(self.options_widgets) except Exception as e: print(f"Warning: Failed to load settings: {e}") @@ -647,11 +650,31 @@ def apply_all_settings(self): cpu_args = [] cpu_governor = self.cpu_widgets['gov_combo'].currentText() + + if self.cpu_widgets.get('max_freq_combo'): + cpu_max_freq = self.cpu_widgets['max_freq_combo'].currentText() + else: + cpu_max_freq = "unset" + + if self.cpu_widgets.get('min_freq_combo'): + cpu_min_freq = self.cpu_widgets['min_freq_combo'].currentText() + else: + cpu_min_freq = "unset" + cpu_scheduler = self.cpu_widgets['sched_combo'].currentText() cpu_parts = [] if cpu_governor != "unset": cpu_parts.append(f"governor:{cpu_governor}") + + if cpu_max_freq != "unset": + max_freq_khz = int(cpu_max_freq) * 1000 + cpu_parts.append(f"max_freq:{max_freq_khz}") + + if cpu_min_freq != "unset": + min_freq_khz = int(cpu_min_freq) * 1000 + cpu_parts.append(f"min_freq:{min_freq_khz}") + if cpu_scheduler != "unset": cpu_parts.append(f"scheduler:{cpu_scheduler}") @@ -741,8 +764,7 @@ def cleanup_resources(self): """ self.save_settings() ConfigManager.save_current_profile_preference(self.current_profile) - if self.options_manager: - self.options_manager.save_options() + OptionsManager.save_options(self.options_widgets) self.instance_checker.cleanup() if self.welcome_window: diff --git a/src/welcome.py b/src/welcome.py index 8cd98a3..d27b008 100644 --- a/src/welcome.py +++ b/src/welcome.py @@ -1,4 +1,4 @@ -from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QLabel, QSizePolicy, QMainWindow, QApplication, QPushButton, QStackedWidget, QFrame, QTextEdit +from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QLabel, QSizePolicy, QMainWindow, QApplication, QPushButton, QStackedWidget, QFrame, QTextEdit, QGraphicsOpacityEffect from PySide6.QtCore import Qt, Signal, QTimer, QPropertyAnimation, QEasingCurve from PySide6.QtGui import QFont, QCursor @@ -37,6 +37,7 @@ class WelcomeManager: "description": "• The apply buttons in the CPU/GPU/Disk/Kernel/Launch Options tabs are interconnected, meaning that pressing one of those apply buttons will apply all settings from these tabs. This helps avoid having to go tab by tab to apply all the settings." "\n\n• The settings that have the `unset` value, or in the case of the Kernel/Launch Options blank space on the text input, will be ignored when pressing apply." "\n\n• Kernel/Disk/CPU settings apply systemwide immediately, while the GPU settings are saved in the `volt` script when you press the apply button." + "\n\n• On the Kernel settings you can modify parameters related to RAM, CPU, Network, and more. I do my best to provide the most accurate recommended values and descriptions possible. If you find any errors or have a way to make them better, please feel free to report them on GitHub." "\n\n• Note: If the OpenGL/Vulkan Render Selector its being used, it might broke some Linux Native games." "\n\n• Note: If you use any of the Render Pipeline Settings, you might have some issues running some games, hopefully mangohud fixes those issues on the future." "\n\n• You can use the Options Tab settings to configure the volt-gui behavior." @@ -348,24 +349,29 @@ def go_next(widgets): @staticmethod def finish_wizard(widgets): """ - Finish the wizard and emit the finished signal if available. + Finish the wizard and close the window. """ - if 'finished_callback' in widgets and widgets['finished_callback']: - widgets['finished_callback']() + if 'welcome_window' in widgets and widgets['welcome_window']: + widgets['welcome_window'].close() @staticmethod - def create_welcome_tab(): + def create_welcome_window(main_window): """ - Creates and returns the welcome tab widget. + Creates a separate welcome window with the welcome wizard. """ - welcome_tab = QWidget() - main_layout = QVBoxLayout(welcome_tab) + welcome_window = QMainWindow() + welcome_window.setWindowTitle("volt-gui - Welcome Setup") + welcome_window.setMinimumSize(540, 380) + welcome_window.setAttribute(Qt.WA_DeleteOnClose, False) + + central_widget = QWidget() + main_layout = QVBoxLayout(central_widget) main_layout.setContentsMargins(16, 16, 16, 16) main_layout.setSpacing(16) widgets = {} widgets['current_step'] = 0 - + widgets['welcome_window'] = welcome_window widgets['stacked_widget'] = QStackedWidget() for i, section in enumerate(WelcomeManager.WELCOME_SECTIONS): @@ -380,21 +386,6 @@ def create_welcome_tab(): widgets['finish_button'].clicked.connect(lambda: WelcomeManager.finish_wizard(widgets)) WelcomeManager.update_navigation(widgets) - - return welcome_tab, widgets - - @staticmethod - def create_welcome_window(main_window): - """ - Creates a separate welcome window with the welcome wizard. - """ - welcome_window = QMainWindow() - welcome_window.setWindowTitle("volt-gui - Welcome Setup") - welcome_window.setMinimumSize(540, 380) - welcome_window.setAttribute(Qt.WA_DeleteOnClose, False) - - welcome_tab, widgets = WelcomeManager.create_welcome_tab() - widgets['finished_callback'] = welcome_window.close - welcome_window.setCentralWidget(welcome_tab) + welcome_window.setCentralWidget(central_widget) return welcome_window \ No newline at end of file