-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
1431 lines (1178 loc) · 68.8 KB
/
main.py
File metadata and controls
1431 lines (1178 loc) · 68.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
# LinuxVitals - System Monitoring and Control Application for Linux
# Copyright (c) 2024 Noel Ejemyr <noelejemyr@protonmail.com>
#
# LinuxVitals is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# LinuxVitals is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import os
import signal
import subprocess
import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk, GLib, Gdk
from core.config_setup import ConfigManager
from core.log_setup import LogSetup
from core.shared import GlobalState, GuiComponents
from ui.create_widgets import WidgetFactory
from utils.cpu_file_search import CPUFileSearch
from core.privileged_actions import PrivilegedActions
from core.apply_settings import SettingsApplier
from ui.settings_window_setup import SettingsWindow
from system.cpu_management import CPUManager
from system.memory_management import MemoryManager
from system.disk_management import DiskManager
from system.process_management import ProcessManager
from system.mounts_management import MountsManager
from system.services_management import ServicesManager
from core.scale_management import ScaleManager
from ui.css_setup import CssManager
from core.task_scheduler import TaskScheduler
from ui.monitor_tab_setup import MonitorTabManager
from system.hardware_detector import HardwareDetector
from ui.dialog_manager import DialogManager
class LinuxVitalsApp(Gtk.Application):
def __init__(self):
super().__init__(application_id="org.LinuxVitals")
# Set WM class so window managers can match this to the .desktop file
GLib.set_prgname("org.LinuxVitals")
GLib.set_application_name("LinuxVitals")
# Set up paths
self.script_dir = os.path.dirname(os.path.abspath(__file__))
self.icon_path = os.path.join(self.script_dir, "icon", "LinuxVitals-Icon.png")
# Initialize core components
self._init_core_components()
# Initialize managers
self._init_managers()
# Initialize UI components
self._init_ui_components()
# Detect hardware capabilities
self.hardware_detector.detect_all_capabilities()
self.is_tdp_installed()
def _init_core_components(self):
"""Initialize core application components"""
self.config_manager = ConfigManager()
self.log_setup = LogSetup(self.config_manager)
self.logger = self.log_setup.logger
self.global_state = GlobalState(self.config_manager, self.logger)
self.gui_components = GuiComponents(self.logger)
self.widget_factory = WidgetFactory(self.logger, self.global_state)
self.cpu_file_search = CPUFileSearch(self.logger)
self.privileged_actions = PrivilegedActions(self.logger)
def _init_managers(self):
"""Initialize all management components"""
self.settings_applier = SettingsApplier(
self.logger, self.global_state, self.gui_components, self.widget_factory,
self.cpu_file_search, self.privileged_actions, self.config_manager)
self.cpu_manager = CPUManager(
self.config_manager, self.logger, self.global_state, self.gui_components,
self.widget_factory, self.cpu_file_search, self.privileged_actions, self.settings_applier)
self.memory_manager = MemoryManager(self.logger, self.config_manager)
self.disk_manager = DiskManager(self.logger, self.config_manager)
self.process_manager = ProcessManager(self.logger, self.config_manager, self.privileged_actions, self.widget_factory)
self.process_manager.initialize_cpu_tracking() # Initialize CPU tracking for accurate percentages
self.mounts_manager = MountsManager(self.logger, self.widget_factory, self.privileged_actions)
self.services_manager = ServicesManager(self.logger, self.widget_factory, self.privileged_actions)
self.scale_manager = ScaleManager(
self.config_manager, self.logger, self.global_state, self.gui_components,
self.widget_factory, self.cpu_file_search, self.cpu_manager)
self.css_manager = CssManager(self.config_manager, self.logger, self.widget_factory)
def _init_ui_components(self):
"""Initialize UI-related components"""
self.settings_window = SettingsWindow(
self.config_manager, self.logger, self.global_state, self.gui_components,
self.widget_factory, self.settings_applier, self.cpu_manager,
self.scale_manager, self.process_manager, self)
self.task_scheduler = TaskScheduler(self.logger)
self.hardware_detector = HardwareDetector(self.logger, self.cpu_file_search)
self.dialog_manager = DialogManager(self.logger, self.widget_factory, self.icon_path)
self.monitor_tab_manager = MonitorTabManager(
self.logger, self.widget_factory, self.cpu_manager,
self.memory_manager, self.disk_manager)
# Pass monitor tab manager to CPU manager for conditional label creation
self.cpu_manager.set_monitor_tab_manager(self.monitor_tab_manager)
def do_activate(self):
"""Application activation - create and show main window"""
try:
self.setup_main_window()
self.create_main_interface()
self.setup_initial_state()
self.window.present()
# Start with monitor tab active
self.on_tab_switch(None, None, 0)
except Exception as e:
self.logger.error(f"Error during application activation: {e}")
def setup_main_window(self):
"""Set up the main application window"""
try:
self.window = self.widget_factory.create_application_window(application=self)
self.window.set_title("LinuxVitals")
# Make window reference available to widget factory for dialogs
self.widget_factory.main_window = self.window
# Apply saved window size if enabled
self.apply_saved_window_size()
# Set minimum window size to prevent widget clipping
self.window.set_size_request(800, 500)
# Set application icon at application level (GTK4 way)
if os.path.exists(self.icon_path):
try:
# GTK4 uses the application icon, not window icon
# We need to register our icon with the icon theme
from gi.repository import Gtk, Gio
icon_theme = Gtk.IconTheme.get_for_display(self.window.get_display())
icon_theme.add_search_path(os.path.join(self.script_dir, "icon"))
self.logger.info(f"Added icon search path: {os.path.join(self.script_dir, 'icon')}")
# Set the icon for the application
icon = Gio.FileIcon.new(Gio.File.new_for_path(self.icon_path))
self.set_icon(icon)
self.logger.info(f"Set application icon from: {self.icon_path}")
# Check if .desktop file is installed
desktop_file = os.path.expanduser("~/.local/share/applications/org.LinuxVitals.desktop")
if not os.path.exists(desktop_file):
self.logger.warning("LinuxVitals .desktop file not found. The icon may not appear in taskbar/panel.")
self.logger.info("Run './install.sh' to install the .desktop file for proper icon display.")
except Exception as e:
self.logger.error(f"Error setting application icon: {e}")
# Connect window size change signal
self.window.connect("notify::default-width", self.on_window_size_changed)
self.window.connect("notify::default-height", self.on_window_size_changed)
except Exception as e:
self.logger.error(f"Error setting up main window: {e}")
def create_main_interface(self):
"""Create the main application interface"""
try:
# Main container
self.main_box = self.widget_factory.create_vertical_box()
self.window.set_child(self.main_box)
# Create content area
self.content_box = self.widget_factory.create_vertical_box(hexpand=True, vexpand=True)
self.main_box.append(self.content_box)
# Create navigation and content
self.create_navigation()
self.create_content_area()
self.create_menu_button()
except Exception as e:
self.logger.error(f"Error creating main interface: {e}")
def create_navigation(self):
"""Create the navigation sidebar"""
try:
# Horizontal box for navigation and content
nav_content_box = self.widget_factory.create_horizontal_box(hexpand=True, vexpand=True)
self.content_box.append(nav_content_box)
# Navigation sidebar
nav_frame = self.widget_factory.create_frame()
nav_frame.set_size_request(150, -1)
nav_content_box.append(nav_frame)
# Navigation list
self.navigation_listbox = self.widget_factory.create_listbox()
self.navigation_listbox.add_css_class('navigation-sidebar')
nav_frame.set_child(self.navigation_listbox)
# Tab information
self.tabs_info = [
("Monitor", "computer-symbolic"),
("Processes", "system-run-symbolic"),
("Mounts", "drive-harddisk-symbolic"),
("Services", "preferences-system-symbolic")
]
# Add control tab if hardware supports it
if self.hardware_detector.show_control_tab:
self.tabs_info.insert(1, ("Control", "preferences-other-symbolic"))
# Create navigation items
for tab_name, icon_name in self.tabs_info:
row = self.widget_factory.create_listbox_row()
content_box = self.widget_factory.create_horizontal_box(
spacing=8, margin_start=8, margin_end=8, margin_top=6, margin_bottom=6)
# Add icon
try:
icon = self.widget_factory.create_image(icon_name, icon_size=Gtk.IconSize.NORMAL)
except:
icon = self.widget_factory.create_image("application-default-symbolic", icon_size=Gtk.IconSize.NORMAL)
content_box.append(icon)
# Add label
label = self.widget_factory.create_label(content_box, text=tab_name)
label.set_halign(Gtk.Align.START)
row.set_child(content_box)
self.navigation_listbox.append(row)
# Set default selection
self.navigation_listbox.select_row(self.navigation_listbox.get_row_at_index(0))
self.navigation_listbox.connect("row-selected", self.on_navigation_selected)
except Exception as e:
self.logger.error(f"Error creating navigation: {e}")
def create_content_area(self):
"""Create the main content area with stack for different tabs"""
try:
# Content stack
self.content_stack = self.widget_factory.create_stack(hexpand=True, vexpand=True)
# Create scrolled windows for each tab
self.monitor_scrolled = self.widget_factory.create_scrolled_window(
policy=(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC),
hexpand=True, vexpand=True)
# Create container boxes for processes and services tabs (button bar + scrolled content)
self.processes_container = self.widget_factory.create_vertical_box(hexpand=True, vexpand=True)
self.services_container = self.widget_factory.create_vertical_box(hexpand=True, vexpand=True)
# Create scrolled windows that will go inside the containers
self.processes_scrolled = self.widget_factory.create_scrolled_window(
policy=(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC),
hexpand=True, vexpand=True)
self.mounts_scrolled = self.widget_factory.create_scrolled_window(
policy=(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC),
hexpand=True, vexpand=True)
self.services_scrolled = self.widget_factory.create_scrolled_window(
policy=(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC),
hexpand=True, vexpand=True)
# Create content boxes
self.monitor_box = self.widget_factory.create_vertical_box(
margin_start=5, margin_end=5, margin_top=5, margin_bottom=5)
self.monitor_scrolled.set_child(self.monitor_box)
# Create grid for mounts tab (processes and services grids created in their widget methods)
self.mounts_grid = self.widget_factory.create_grid()
self.mounts_scrolled.set_child(self.mounts_grid)
# Add to stack
self.content_stack.add_named(self.monitor_scrolled, "monitor")
self.content_stack.add_named(self.processes_container, "processes")
self.content_stack.add_named(self.mounts_scrolled, "mounts")
self.content_stack.add_named(self.services_container, "services")
# Add control tab if supported
if self.hardware_detector.show_control_tab:
self.control_scrolled = self.widget_factory.create_scrolled_window(
policy=(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC),
hexpand=True, vexpand=True)
self.control_box = self.widget_factory.create_vertical_box(
margin_start=5, margin_end=5, margin_top=5, margin_bottom=5)
self.control_scrolled.set_child(self.control_box)
self.content_stack.add_named(self.control_scrolled, "control")
# Add stack to main content
nav_content_box = self.content_box.get_first_child()
nav_content_box.append(self.content_stack)
except Exception as e:
self.logger.error(f"Error creating content area: {e}")
def create_menu_button(self):
"""Create the menu button"""
try:
# Header for content area with menu button
content_header = self.widget_factory.create_horizontal_box(
margin_start=5, margin_end=5, margin_top=3, margin_bottom=3)
# Spacer to push menu button to the right
spacer = self.widget_factory.create_horizontal_box(hexpand=True)
content_header.append(spacer)
# Menu button
self.more_button = self.widget_factory.create_button(
content_header, "", self.show_more_options)
self.more_button.set_icon_name("open-menu-symbolic")
# Insert header at top of content area
self.content_box.prepend(content_header)
except Exception as e:
self.logger.error(f"Error creating menu button: {e}")
def setup_initial_state(self):
"""Set up the initial application state"""
try:
# Initialize theme preference from GNOME settings
self.setup_theme_preference()
# Create widgets for each tab
self.create_tab_widgets()
# Add widgets to GUI components
self.add_widgets_to_gui_components()
# Apply CSS styling
self.css_manager.apply_custom_styles()
# Initialize dark mode checkbutton state to match current theme
self.settings_window.init_dark_mode_setting()
except Exception as e:
self.logger.error(f"Error setting up initial state: {e}")
def setup_theme_preference(self):
"""Setup theme preference based on GNOME settings"""
try:
self.logger.info("Setting up theme preference...")
# Get the default GTK settings
settings = Gtk.Settings.get_default()
if not settings:
self.logger.error("Failed to get GTK settings")
return
# Check if we have a user override in config
user_preference = self.config_manager.get_setting("UI", "prefer_dark_theme", None)
if user_preference is not None:
# User has explicitly set a preference, use that
prefer_dark = user_preference.lower() == 'true'
self.logger.info(f"Using user theme preference: dark={prefer_dark}")
else:
# Try to detect system preference first
prefer_dark = self._detect_system_theme_preference()
if prefer_dark is None:
# No system preference detected, default to dark theme since user prefers it
prefer_dark = True
self.logger.info("No system preference detected, defaulting to dark theme")
else:
self.logger.info(f"Detected system theme preference: dark={prefer_dark}")
# Apply the theme preference
current_value = settings.get_property("gtk-application-prefer-dark-theme")
self.logger.info(f"Current GTK dark theme setting: {current_value}")
settings.set_property("gtk-application-prefer-dark-theme", prefer_dark)
# Verify the setting was applied
new_value = settings.get_property("gtk-application-prefer-dark-theme")
self.logger.info(f"Applied theme preference: dark={prefer_dark}, verified: {new_value}")
# Force a style refresh (GTK4 handles this automatically)
except Exception as e:
self.logger.error(f"Error setting up theme preference: {e}")
def _detect_system_theme_preference(self):
"""Detect system theme preference using multiple methods"""
try:
# Method 1: Try GTK_THEME environment variable
gtk_theme = os.environ.get('GTK_THEME', '')
if 'dark' in gtk_theme.lower():
return True
elif gtk_theme and 'light' in gtk_theme.lower():
return False
# Method 2: Try reading gsettings if available
try:
import subprocess
result = subprocess.run(['gsettings', 'get', 'org.gnome.desktop.interface', 'color-scheme'],
capture_output=True, text=True, timeout=2)
if result.returncode == 0:
color_scheme = result.stdout.strip().strip("'\"")
if 'dark' in color_scheme.lower():
return True
elif 'light' in color_scheme.lower():
return False
except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError):
pass
# Method 3: Check for common dark theme indicators
theme_indicators = [
os.environ.get('DESKTOP_SESSION', ''),
os.environ.get('XDG_CURRENT_DESKTOP', ''),
os.environ.get('QT_STYLE_OVERRIDE', '')
]
for indicator in theme_indicators:
if indicator and 'dark' in indicator.lower():
return True
except Exception as e:
self.logger.warning(f"Error detecting system theme: {e}")
return None # Could not detect
def apply_saved_window_size(self):
"""Apply saved window size if the feature is enabled"""
try:
remember_size = self.config_manager.get_setting("UI", "remember_window_size", "true").lower() == "true"
if remember_size:
saved_width = int(self.config_manager.get_setting("UI", "window_width", "850"))
saved_height = int(self.config_manager.get_setting("UI", "window_height", "550"))
# Ensure reasonable minimum sizes
width = max(800, saved_width)
height = max(500, saved_height)
self.window.set_default_size(width, height)
self.logger.info(f"Applied saved window size: {width}x{height}")
else:
# Use default size
self.window.set_default_size(850, 550)
self.logger.info("Using default window size: 850x550")
except (ValueError, Exception) as e:
self.logger.warning(f"Error applying saved window size, using defaults: {e}")
self.window.set_default_size(850, 550)
def on_window_size_changed(self, window, param):
"""Handle window size changes and save if enabled"""
try:
remember_size = self.config_manager.get_setting("UI", "remember_window_size", "true").lower() == "true"
if remember_size:
width, height = window.get_default_size()
if width > 0 and height > 0:
self.config_manager.set_setting("UI", "window_width", str(width))
self.config_manager.set_setting("UI", "window_height", str(height))
self.logger.info(f"Saved window size: {width}x{height}")
except Exception as e:
self.logger.warning(f"Error saving window size: {e}")
def create_tab_widgets(self):
"""Create widgets for all tabs"""
try:
# Monitor tab
self.monitor_tab_manager.create_monitor_widgets(self.monitor_box)
self.clock_labels = self.monitor_tab_manager.clock_labels
self.usage_labels = self.monitor_tab_manager.usage_labels
self.cpu_graphs = self.monitor_tab_manager.cpu_graphs
self.avg_usage_graph = self.monitor_tab_manager.avg_usage_graph
self.avg_clock_label = self.monitor_tab_manager.avg_clock_label
self.avg_usage_label = self.monitor_tab_manager.avg_usage_label
self.package_temp_label = self.monitor_tab_manager.package_temp_label
self.current_governor_label = self.monitor_tab_manager.current_governor_label
self.thermal_throttle_label = self.monitor_tab_manager.thermal_throttle_label
# Processes tab
self.create_processes_widgets()
# Mounts tab
self.create_mounts_widgets()
# Services tab
self.create_services_widgets()
# Control tab (if supported)
if self.hardware_detector.show_control_tab and hasattr(self, 'control_box'):
self.create_control_widgets()
except Exception as e:
self.logger.error(f"Error creating tab widgets: {e}")
def create_processes_widgets(self):
"""Create widgets for the processes tab"""
try:
# Process menu bar (at top of container, outside scrolled window)
self.create_process_menu_bar()
# Add scrolled window to container
self.processes_container.append(self.processes_scrolled)
# Create grid inside scrolled window for tree view
self.processes_grid = self.widget_factory.create_grid()
self.processes_scrolled.set_child(self.processes_grid)
# Process tree view (inside scrolled window)
process_tree = self.process_manager.create_process_tree_view()
if process_tree:
self.processes_grid.attach(process_tree, 0, 0, 1, 1)
# Connect selection change handler
self.process_manager.set_selection_changed_callback(self.on_process_selection_changed)
except Exception as e:
self.logger.error(f"Error creating processes widgets: {e}")
def create_process_menu_bar(self):
"""Create the process management menu bar"""
try:
menu_bar = self.widget_factory.create_horizontal_box(spacing=5, margin_start=5, margin_top=5, margin_bottom=5)
# Process action buttons
self.end_button = self.widget_factory.create_button(menu_bar, "End", self.end_selected_process)
self.kill_button = self.widget_factory.create_button(menu_bar, "Kill", self.kill_selected_process)
self.stop_button = self.widget_factory.create_button(menu_bar, "Stop", self.stop_selected_process)
self.properties_button = self.widget_factory.create_button(menu_bar, "Properties", self.show_selected_process_properties)
# Initially hide all process buttons
self.set_process_buttons_visible(False)
# Add menu bar to container (outside scrolled window)
self.processes_container.append(menu_bar)
except Exception as e:
self.logger.error(f"Error creating process menu bar: {e}")
def create_mounts_widgets(self):
"""Create widgets for the mounts tab"""
try:
# Mounts tree view
mounts_tree = self.mounts_manager.create_mounts_tree_view()
if mounts_tree:
self.mounts_grid.attach(mounts_tree, 0, 0, 1, 1)
except Exception as e:
self.logger.error(f"Error creating mounts widgets: {e}")
def create_services_widgets(self):
"""Create widgets for the services tab"""
try:
# Services menu bar (at top of container, outside scrolled window)
self.create_services_menu_bar()
# Add scrolled window to container
self.services_container.append(self.services_scrolled)
# Create grid inside scrolled window for tree view and filter controls
self.services_grid = self.widget_factory.create_grid()
self.services_scrolled.set_child(self.services_grid)
# Services tree view (inside scrolled window)
services_tree = self.services_manager.create_services_tree_view()
if services_tree:
self.services_grid.attach(services_tree, 0, 0, 1, 1)
# Connect selection change handler
self.services_manager.set_selection_changed_callback(self.on_service_selection_changed)
# Services filter controls (at bottom, inside scrolled window)
self.create_services_filter_controls()
except Exception as e:
self.logger.error(f"Error creating services widgets: {e}")
def create_services_filter_controls(self):
"""Create filter controls for services"""
try:
filter_box = self.widget_factory.create_horizontal_box(spacing=10, margin_start=5, margin_top=5)
# Filter checkboxes
self.systemd_check = self.widget_factory.create_checkbutton(filter_box, "Systemd Services", True)
self.autostart_check = self.widget_factory.create_checkbutton(filter_box, "Autostart Applications", True)
self.running_only_check = self.widget_factory.create_checkbutton(filter_box, "Running Only", False)
# Connect signals
self.systemd_check.connect("toggled", self.on_filter_changed)
self.autostart_check.connect("toggled", self.on_filter_changed)
self.running_only_check.connect("toggled", self.on_filter_changed)
self.services_grid.attach(filter_box, 0, 1, 1, 1)
except Exception as e:
self.logger.error(f"Error creating services filter controls: {e}")
def create_services_menu_bar(self):
"""Create the services management menu bar"""
try:
menu_bar = self.widget_factory.create_horizontal_box(spacing=5, margin_start=5, margin_top=5, margin_bottom=5)
# Service action buttons
self.service_start_button = self.widget_factory.create_button(menu_bar, "Start", self.start_selected_service)
self.service_stop_button = self.widget_factory.create_button(menu_bar, "Stop", self.stop_selected_service)
self.service_restart_button = self.widget_factory.create_button(menu_bar, "Restart", self.restart_selected_service)
self.service_enable_button = self.widget_factory.create_button(menu_bar, "Enable", self.enable_selected_service)
self.service_disable_button = self.widget_factory.create_button(menu_bar, "Disable", self.disable_selected_service)
self.service_properties_button = self.widget_factory.create_button(menu_bar, "Properties", self.show_selected_service_properties)
# Initially hide all service buttons
self.set_service_buttons_visible(False)
# Add menu bar to container (outside scrolled window)
self.services_container.append(menu_bar)
except Exception as e:
self.logger.error(f"Error creating services menu bar: {e}")
def create_control_widgets(self):
"""Create widgets for the control tab"""
try:
# Create CPU frequency control section
self.create_frequency_control_section()
# Create CPU governor control section
self.create_governor_control_section()
# Create CPU boost control section (only if boost is supported)
if self.cpu_manager.is_boost_supported():
self.create_boost_control_section()
# Create TDP control section (Intel/AMD)
self.create_tdp_control_section()
# Create PBO control section (AMD)
self.create_pbo_control_section()
# Create EPB control section (Intel)
self.create_epb_control_section()
except Exception as e:
self.logger.error(f"Error creating control widgets: {e}")
def create_frequency_control_section(self):
"""Create CPU frequency control widgets using scale manager"""
try:
# Frequency control frame
freq_frame = self.widget_factory.create_frame()
freq_frame.set_label("CPU Frequency Control")
self.control_box.append(freq_frame)
freq_box = self.widget_factory.create_vertical_box(margin_start=10, margin_end=10, margin_top=10, margin_bottom=10, spacing=10)
freq_frame.set_child(freq_box)
# Select all threads control
select_all_box = self.widget_factory.create_horizontal_box(spacing=10)
freq_box.append(select_all_box)
self.select_all_threads_checkbutton = self.widget_factory.create_checkbutton(
select_all_box, "Select All Threads", True, self.on_select_all_threads_toggled)
# Sync scales checkbutton in the same row
self.sync_scales_checkbutton = self.widget_factory.create_checkbutton(
select_all_box, "Sync Scales", self.global_state.sync_scales, self.scale_manager.on_sync_scales_change)
# Apply button in the same row
self.apply_max_min_button = self.widget_factory.create_button(
select_all_box, "Apply Frequency Limits", self.cpu_manager.apply_cpu_clock_speed_limits)
self.apply_max_min_button.set_hexpand(False)
# Create a flow box for thread controls (similar to monitor tab)
threads_flow = self.widget_factory.create_flowbox(
valign=Gtk.Align.START,
max_children_per_line=6, # More flexible layout
min_children_per_line=2,
row_spacing=10,
column_spacing=10,
homogeneous=True,
selection_mode=Gtk.SelectionMode.NONE)
freq_box.append(threads_flow)
# Initialize storage
self.cpu_max_min_checkbuttons = {}
self.min_scales = {}
self.max_scales = {}
self.min_freq_labels = {} # Store min frequency labels
self.max_freq_labels = {} # Store max frequency labels
# Get cached frequency limits from scale manager
min_freqs, max_freqs = self.scale_manager._cached_freqs or ([1000] * self.cpu_file_search.thread_count, [3000] * self.cpu_file_search.thread_count)
# Create thread control boxes
for i in range(self.cpu_file_search.thread_count):
# Create frame for each thread
thread_frame = self.widget_factory.create_frame()
# Restore size constraints to prevent label corruption
thread_frame.set_size_request(180, 160)
threads_flow.append(thread_frame)
thread_box = self.widget_factory.create_vertical_box(margin_start=8, margin_end=8, margin_top=5, margin_bottom=5, spacing=8)
thread_frame.set_child(thread_box)
# Thread header with enable checkbox
header_box = self.widget_factory.create_horizontal_box()
thread_box.append(header_box)
self.cpu_max_min_checkbuttons[i] = self.widget_factory.create_checkbutton(
header_box, f"CPU {i}", True)
self.cpu_max_min_checkbuttons[i].add_css_class('small-label')
# Connect signal to sync with "Select All Threads" checkbox
self.cpu_max_min_checkbuttons[i].connect("toggled", self.on_individual_thread_toggled)
# Get frequency limits for this thread
min_freq = min_freqs[i] if i < len(min_freqs) else 1000
max_freq = max_freqs[i] if i < len(max_freqs) else 3000
# Min frequency section
min_section = self.widget_factory.create_vertical_box(spacing=2)
thread_box.append(min_section)
min_header = self.widget_factory.create_horizontal_box()
min_section.append(min_header)
min_title = self.widget_factory.create_label(min_header, "Minimum:")
min_title.set_halign(Gtk.Align.START)
min_title.add_css_class('small-label')
# Spacer to push frequency label to the right
min_spacer = self.widget_factory.create_horizontal_box(hexpand=True)
min_header.append(min_spacer)
# Create label with proper MHz/GHz display
if self.global_state.display_ghz:
min_freq_label = self.widget_factory.create_label(min_header, f"{min_freq/1000:.2f} GHz")
else:
min_freq_label = self.widget_factory.create_label(min_header, f"{min_freq:.0f} MHz")
min_freq_label.set_halign(Gtk.Align.END)
min_freq_label.set_ellipsize(3) # Pango.EllipsizeMode.END
min_freq_label.set_size_request(80, -1) # Minimum width for frequency display
min_freq_label.add_css_class('small-label')
min_freq_label.set_name(f"min_freq_label_{i}") # Add name for later reference
self.min_freq_labels[i] = min_freq_label # Store reference
# Min frequency scale - create without dynamic label
adjustment = self.widget_factory.create_adjustment(lower=min_freq, upper=max_freq, step_increment=1)
scale = self.widget_factory.create_scale_widget(orientation=Gtk.Orientation.HORIZONTAL, adjustment=adjustment)
scale.set_draw_value(False)
scale.set_name(f"cpu_min_scale_{i}")
scale.set_value(min_freq)
scale.set_size_request(160, 30) # Fixed size needed for stable label rendering
scale.connect("value-changed", self.scale_manager.update_min_max_labels)
min_section.append(scale)
self.min_scales[i] = scale
# Connect to update frequency label with MHz/GHz support
def make_min_freq_updater(label, global_state):
def update_min_freq_label(scale):
value = scale.get_value()
if global_state.display_ghz:
label.set_text(f"{value/1000:.2f} GHz")
else:
label.set_text(f"{value:.0f} MHz")
return update_min_freq_label
scale.connect("value-changed", make_min_freq_updater(min_freq_label, self.global_state))
# Add some spacing between min and max sections
spacer = self.widget_factory.create_vertical_box()
spacer.set_size_request(-1, 5) # 5px vertical space
thread_box.append(spacer)
# Max frequency section
max_section = self.widget_factory.create_vertical_box(spacing=2)
thread_box.append(max_section)
max_header = self.widget_factory.create_horizontal_box()
max_section.append(max_header)
max_title = self.widget_factory.create_label(max_header, "Maximum:")
max_title.set_halign(Gtk.Align.START)
max_title.add_css_class('small-label')
# Spacer to push frequency label to the right
max_spacer = self.widget_factory.create_horizontal_box(hexpand=True)
max_header.append(max_spacer)
# Create label with proper MHz/GHz display
if self.global_state.display_ghz:
max_freq_label = self.widget_factory.create_label(max_header, f"{max_freq/1000:.2f} GHz")
else:
max_freq_label = self.widget_factory.create_label(max_header, f"{max_freq:.0f} MHz")
max_freq_label.set_halign(Gtk.Align.END)
max_freq_label.set_ellipsize(3) # Pango.EllipsizeMode.END
max_freq_label.set_size_request(80, -1) # Minimum width for frequency display
max_freq_label.add_css_class('small-label')
max_freq_label.set_name(f"max_freq_label_{i}") # Add name for later reference
self.max_freq_labels[i] = max_freq_label # Store reference
# Max frequency scale - create without dynamic label
adjustment = self.widget_factory.create_adjustment(lower=min_freq, upper=max_freq, step_increment=1)
scale = self.widget_factory.create_scale_widget(orientation=Gtk.Orientation.HORIZONTAL, adjustment=adjustment)
scale.set_draw_value(False)
scale.set_name(f"cpu_max_scale_{i}")
scale.set_value(max_freq)
scale.set_size_request(160, 30) # Fixed size needed for stable label rendering
scale.connect("value-changed", self.scale_manager.update_min_max_labels)
max_section.append(scale)
self.max_scales[i] = scale
# Connect to update frequency label with MHz/GHz support
def make_max_freq_updater(label, global_state):
def update_max_freq_label(scale):
value = scale.get_value()
if global_state.display_ghz:
label.set_text(f"{value/1000:.2f} GHz")
else:
label.set_text(f"{value:.0f} MHz")
return update_max_freq_label
scale.connect("value-changed", make_max_freq_updater(max_freq_label, self.global_state))
except Exception as e:
self.logger.error(f"Error creating frequency control section: {e}")
def on_select_all_threads_toggled(self, checkbutton):
"""Handle select all threads checkbox toggle"""
try:
select_all = checkbutton.get_active()
# Temporarily block individual thread signals to avoid infinite loop
for i in range(self.cpu_file_search.thread_count):
if i in self.cpu_max_min_checkbuttons:
self.cpu_max_min_checkbuttons[i].handler_block_by_func(self.on_individual_thread_toggled)
self.cpu_max_min_checkbuttons[i].set_active(select_all)
self.cpu_max_min_checkbuttons[i].handler_unblock_by_func(self.on_individual_thread_toggled)
except Exception as e:
self.logger.error(f"Error toggling select all threads: {e}")
def on_individual_thread_toggled(self, checkbutton):
"""Handle individual thread checkbox toggle - sync with Select All checkbox"""
try:
# Check if all individual thread checkboxes are active
all_active = True
for i in range(self.cpu_file_search.thread_count):
if i in self.cpu_max_min_checkbuttons:
if not self.cpu_max_min_checkbuttons[i].get_active():
all_active = False
break
# Update "Select All Threads" checkbox to match
# Block the signal to avoid infinite loop
self.select_all_threads_checkbutton.handler_block_by_func(self.on_select_all_threads_toggled)
self.select_all_threads_checkbutton.set_active(all_active)
self.select_all_threads_checkbutton.handler_unblock_by_func(self.on_select_all_threads_toggled)
except Exception as e:
self.logger.error(f"Error syncing individual thread toggle: {e}")
def create_governor_control_section(self):
"""Create CPU governor control widgets"""
try:
# Governor control frame
gov_frame = self.widget_factory.create_frame()
gov_frame.set_label("CPU Governor Control")
self.control_box.append(gov_frame)
gov_box = self.widget_factory.create_vertical_box(margin_start=10, margin_end=10, margin_top=10, margin_bottom=10, spacing=10)
gov_frame.set_child(gov_box)
# Governor dropdown
gov_label = self.widget_factory.create_label(gov_box, "Select CPU Governor:")
# Create dropdown with placeholder
governors_list = ["Select Governor"]
self.governor_dropdown = self.widget_factory.create_dropdown(gov_box, governors_list, self.cpu_manager.set_cpu_governor)
# Assign dropdown to CPU manager before updating
self.cpu_manager.governor_dropdown = self.governor_dropdown
# Update with available governors - schedule it to run after the UI is fully created
GLib.idle_add(self.cpu_manager.update_governor_dropdown)
except Exception as e:
self.logger.error(f"Error creating governor control section: {e}")
def create_boost_control_section(self):
"""Create CPU boost control widgets"""
try:
# Boost control frame
boost_frame = self.widget_factory.create_frame()
boost_frame.set_label("CPU Boost Control")
self.control_box.append(boost_frame)
boost_box = self.widget_factory.create_vertical_box(margin_start=10, margin_end=10, margin_top=10, margin_bottom=10)
boost_frame.set_child(boost_box)
# Boost checkbox
self.boost_checkbutton = self.widget_factory.create_checkbutton(boost_box, "Enable CPU Boost", False)
self.boost_checkbutton.connect("toggled", self.cpu_manager.toggle_boost)
except Exception as e:
self.logger.error(f"Error creating boost control section: {e}")
def create_tdp_control_section(self):
"""Create TDP control widgets"""
try:
# Check if TDP control is available
if self.cpu_file_search.cpu_type == "Intel":
max_tdp = self.cpu_manager.get_allowed_tdp_values()
if max_tdp:
self.create_intel_tdp_widgets(max_tdp)
elif self.cpu_file_search.cpu_type == "Other" and self.global_state.is_ryzen_smu_installed():
self.create_amd_tdp_widgets()
except Exception as e:
self.logger.error(f"Error creating TDP control section: {e}")
def create_intel_tdp_widgets(self, max_tdp):
"""Create Intel TDP control widgets"""
try:
# Intel TDP control frame
tdp_frame = self.widget_factory.create_frame()
tdp_frame.set_label("Intel TDP Control")
self.control_box.append(tdp_frame)
tdp_box = self.widget_factory.create_vertical_box(margin_start=10, margin_end=10, margin_top=10, margin_bottom=10, spacing=10)
tdp_frame.set_child(tdp_box)
# TDP scale
tdp_label = self.widget_factory.create_label(tdp_box, f"TDP: {max_tdp:.1f} W")
self.tdp_scale = self.widget_factory.create_scale(tdp_box, None, 5.0, max_tdp)
# Update label on value change and set initial value
if self.tdp_scale:
self.tdp_scale.set_value(max_tdp)
def update_tdp_label(scale):
tdp_label.set_text(f"TDP: {scale.get_value():.1f} W")
self.tdp_scale.connect("value-changed", update_tdp_label)
# Apply button
self.apply_tdp_button = self.widget_factory.create_button(tdp_box, "Apply TDP", self.cpu_manager.set_intel_tdp)
except Exception as e:
self.logger.error(f"Error creating Intel TDP widgets: {e}")
def create_amd_tdp_widgets(self):
"""Create AMD TDP control widgets"""
try:
# AMD TDP control frame
tdp_frame = self.widget_factory.create_frame()
tdp_frame.set_label("AMD Ryzen TDP Control")
self.control_box.append(tdp_frame)
tdp_box = self.widget_factory.create_vertical_box(margin_start=10, margin_end=10, margin_top=10, margin_bottom=10, spacing=10)
tdp_frame.set_child(tdp_box)
# TDP scale (common range for AMD)
tdp_label = self.widget_factory.create_label(tdp_box, "TDP: 65.0 W")
self.tdp_scale = self.widget_factory.create_scale(tdp_box, None, 15.0, 200.0)
# Update label on value change and set initial value
if self.tdp_scale:
self.tdp_scale.set_value(65.0)
def update_tdp_label(scale):
tdp_label.set_text(f"TDP: {scale.get_value():.1f} W")
self.tdp_scale.connect("value-changed", update_tdp_label)
# Apply button
self.apply_tdp_button = self.widget_factory.create_button(tdp_box, "Apply TDP", self.cpu_manager.set_ryzen_tdp)
except Exception as e:
self.logger.error(f"Error creating AMD TDP widgets: {e}")
def create_pbo_control_section(self):
"""Create PBO curve control widgets (AMD only)"""
try:
if self.cpu_file_search.cpu_type == "Other" and self.global_state.is_ryzen_smu_installed():
# PBO control frame
pbo_frame = self.widget_factory.create_frame()
pbo_frame.set_label("AMD PBO Curve Optimizer")
self.control_box.append(pbo_frame)
pbo_box = self.widget_factory.create_vertical_box(margin_start=10, margin_end=10, margin_top=10, margin_bottom=10, spacing=10)
pbo_frame.set_child(pbo_box)