Files
Modus/config/settings/gui.py
T

365 lines
15 KiB
Python

import json
import os
import subprocess
import threading
import time
import gi
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.entry import Entry
from fabric.widgets.image import Image as FabricImage
from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow
from fabric.widgets.stack import Stack
from fabric.widgets.window import Window
from gi.repository import Gdk, GdkPixbuf, GLib, Gtk
from PIL import Image
from config.data import APP_NAME, APP_NAME_CAP
from config.settings.utils import backup_and_replace, start_config
from config.settings.wifi_tab import WiFiTab
from config.settings.bluetooth_tab import BluetoothTab
from config.settings.key_bindings_tab import KeyBindingsTab
from config.settings.appearance_tab import AppearanceTab
from config.settings.system_tab import SystemTab
from config.settings.about_tab import AboutTab
gi.require_version("Gtk", "3.0")
class HyprConfGUI(Window):
def __init__(self, show_lock_checkbox: bool, show_idle_checkbox: bool, **kwargs):
super().__init__(
title="Modus Settings",
name="modus-settings-window",
size=(640, 640),
**kwargs,
)
self.set_resizable(False)
# Set strict size constraints to prevent dynamic resizing
self.set_size_request(640, 640)
self.set_default_size(640, 640)
# Set geometry hints to enforce fixed size
geometry = Gdk.Geometry()
geometry.min_width = 640
geometry.max_width = 640
geometry.min_height = 640
geometry.max_height = 640
self.set_geometry_hints(None, geometry,
Gdk.WindowHints.MIN_SIZE | Gdk.WindowHints.MAX_SIZE)
self.themes = ["Pills", "Dense", "Edge"]
self.selected_face_icon = None
self.show_lock_checkbox = show_lock_checkbox
self.show_idle_checkbox = show_idle_checkbox
# Initialize all tabs
self.wifi_tab = WiFiTab()
self.bluetooth_tab = BluetoothTab()
self.key_bindings_tab = KeyBindingsTab()
self.appearance_tab = AppearanceTab(self.themes, self)
self.system_tab = SystemTab(show_lock_checkbox, show_idle_checkbox, self.enforce_window_size)
self.about_tab = AboutTab()
# Pass window size enforcement method to tabs
self.wifi_tab.set_window_size_enforcer(self.enforce_window_size)
self.bluetooth_tab.set_window_size_enforcer(self.enforce_window_size)
root_box = Box(orientation="v", spacing=10, style="margin: 10px;")
# Set fixed size for root container
root_box.set_size_request(620, 620)
self.add(root_box)
main_content_box = Box(orientation="h", spacing=6, v_expand=False, h_expand=False)
# Set fixed size for main content to prevent expansion
main_content_box.set_size_request(620, 580)
root_box.add(main_content_box)
self.tab_stack = Stack(
transition_type="slide-up-down",
transition_duration=250,
v_expand=False,
h_expand=False,
)
# Set fixed size for tab stack
self.tab_stack.set_size_request(580, 580)
self.wifi_tab_content = self.wifi_tab.create_wifi_tab()
self.bluetooth_tab_content = self.bluetooth_tab.create_bluetooth_tab()
self.key_bindings_tab_content = self.key_bindings_tab.create_key_bindings_tab()
self.appearance_tab_content = self.appearance_tab.create_appearance_tab()
self.system_tab_content = self.system_tab.create_system_tab()
self.about_tab_content = self.about_tab.create_about_tab()
self.tab_stack.add_titled(
self.wifi_tab_content, "Wifi", "Wifi"
)
self.tab_stack.add_titled(
self.bluetooth_tab_content, "Bluetooth", "Bluetooth"
)
self.tab_stack.add_titled(
self.key_bindings_tab_content, "key_bindings", "Key Bindings"
)
self.tab_stack.add_titled(
self.appearance_tab_content, "appearance", "Appearance"
)
self.tab_stack.add_titled(self.system_tab_content, "system", "System")
self.tab_stack.add_titled(self.about_tab_content, "about", "About")
tab_switcher = Gtk.StackSwitcher()
tab_switcher.set_stack(self.tab_stack)
tab_switcher.set_orientation(Gtk.Orientation.VERTICAL)
# Set fixed width for tab switcher
tab_switcher.set_size_request(40, -1)
main_content_box.add(tab_switcher)
main_content_box.add(self.tab_stack)
button_box = Box(orientation="h", spacing=10, h_align="end")
# Set fixed height for button box
button_box.set_size_request(-1, 40)
reset_btn = Button(label="Reset to Defaults", on_clicked=self.on_reset)
button_box.add(reset_btn)
close_btn = Button(label="Close", on_clicked=self.on_close)
button_box.add(close_btn)
accept_btn = Button(label="Apply & Reload", on_clicked=self.on_accept)
button_box.add(accept_btn)
root_box.add(button_box)
# Force window size after all content is added and on any size changes
self.connect("realize", self._force_window_size)
self.connect("size-allocate", self._on_size_allocate)
def _force_window_size(self, widget):
"""Force the window to respect the declared size"""
self.resize(640, 640)
self.set_size_request(640, 640)
def _on_size_allocate(self, widget, allocation):
"""Prevent window from changing size during allocation"""
if allocation.width != 640 or allocation.height != 640:
GLib.idle_add(lambda: self.resize(640, 640))
def enforce_window_size(self):
"""Public method to enforce window size - can be called by tabs"""
self._force_window_size(self)
# Removed create_appearance_tab - now using AppearanceTab class
# on_notification_position_changed moved to AppearanceTab class
# Removed create_system_tab - now using SystemTab class
# Removed create_about_tab - now using AboutTab class
# Event handlers moved to respective tab classes
def on_accept(self, _widget):
current_bind_vars_snapshot = {}
# Get values from key bindings tab
current_bind_vars_snapshot.update(self.key_bindings_tab.get_key_binding_values())
# Get values from appearance tab
current_bind_vars_snapshot.update(self.appearance_tab.get_appearance_values())
# Get values from system tab
current_bind_vars_snapshot.update(self.system_tab.get_system_values())
# Get face icon and hyprland switches
selected_icon_path = self.appearance_tab.get_selected_face_icon()
hyprland_switches = self.system_tab.get_hyprland_switches()
replace_lock = hyprland_switches["replace_lock"]
replace_idle = hyprland_switches["replace_idle"]
if selected_icon_path:
self.appearance_tab.clear_selected_face_icon()
def _apply_and_reload_task_thread():
nonlocal current_bind_vars_snapshot
from . import utils
utils.bind_vars.clear()
utils.bind_vars.update(current_bind_vars_snapshot)
start_time = time.time()
print(f"{start_time:.4f}: Background task started.")
config_json = os.path.expanduser(
f"~/.config/{APP_NAME_CAP}/config/assets/config.json"
)
os.makedirs(os.path.dirname(config_json), exist_ok=True)
try:
with open(config_json, "w") as f:
json.dump(utils.bind_vars, f, indent=4)
print(f"{time.time():.4f}: Saved config.json.")
except Exception as e:
print(f"Error saving config.json: {e}")
if selected_icon_path:
print(f"{time.time():.4f}: Processing face icon...")
try:
img = Image.open(selected_icon_path)
side = min(img.size)
left = (img.width - side) // 2
top = (img.height - side) // 2
cropped_img = img.crop((left, top, left + side, top + side))
face_icon_dest = os.path.expanduser("~/.face.icon")
cropped_img.save(face_icon_dest, format="PNG")
print(f"{time.time():.4f}: Face icon saved to {face_icon_dest}")
GLib.idle_add(self._update_face_image_widget, face_icon_dest)
except Exception as e:
print(f"Error processing face icon: {e}")
print(f"{time.time():.4f}: Finished processing face icon.")
if replace_lock:
print(f"{time.time():.4f}: Replacing hyprlock config...")
src = os.path.expanduser(
f"~/.config/{APP_NAME_CAP}/config/hypr/hyprlock.conf"
)
dest = os.path.expanduser("~/.config/hypr/hyprlock.conf")
if os.path.exists(src):
backup_and_replace(src, dest, "Hyprlock")
else:
print(f"Warning: Source hyprlock config not found at {src}")
print(f"{time.time():.4f}: Finished replacing hyprlock config.")
if replace_idle:
print(f"{time.time():.4f}: Replacing hypridle config...")
src = os.path.expanduser(
f"~/.config/{APP_NAME_CAP}/config/hypr/hypridle.conf"
)
dest = os.path.expanduser("~/.config/hypr/hypridle.conf")
if os.path.exists(src):
backup_and_replace(src, dest, "Hypridle")
else:
print(f"Warning: Source hypridle config not found at {src}")
print(f"{time.time():.4f}: Finished replacing hypridle config.")
print(
f"{time.time():.4f}: Checking/Appending hyprland.conf source string..."
)
hypr_path = os.path.expanduser("~/.config/hypr/hyprland.conf")
try:
from .constants import SOURCE_STRING
needs_append = True
if os.path.exists(hypr_path):
with open(hypr_path, "r") as f:
if SOURCE_STRING.strip() in f.read():
needs_append = False
else:
os.makedirs(os.path.dirname(hypr_path), exist_ok=True)
if needs_append:
with open(hypr_path, "a") as f:
f.write("\n" + SOURCE_STRING)
print(f"Appended source string to {hypr_path}")
except Exception as e:
print(f"Error updating {hypr_path}: {e}")
print(
f"{time.time():.4f}: Finished checking/appending hyprland.conf source string."
)
print(f"{time.time():.4f}: Running start_config()...")
start_config()
print(f"{time.time():.4f}: Finished start_config().")
print(f"{time.time():.4f}: Initiating Modus restart using Popen...")
main_py = os.path.expanduser(f"~/.config/{APP_NAME_CAP}/main.py")
kill_cmd = f"killall {APP_NAME}"
start_cmd = ["uwsm", "app", "--", "python", main_py]
try:
kill_proc = subprocess.Popen(
kill_cmd,
shell=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
kill_proc.wait(timeout=2)
print(f"{time.time():.4f}: killall process finished (or timed out).")
except subprocess.TimeoutExpired:
print("Warning: killall command timed out.")
except Exception as e:
print(f"Error running killall: {e}")
try:
subprocess.Popen(
start_cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
print(f"{APP_NAME_CAP} restart initiated via Popen.")
except FileNotFoundError as e:
print(f"Error restarting {APP_NAME_CAP}: Command not found ({e})")
except Exception as e:
print(f"Error restarting {APP_NAME_CAP} via Popen: {e}")
print(f"{time.time():.4f}: Modus restart commands issued via Popen.")
end_time = time.time()
total_time = end_time - start_time
print(
f"{end_time:.4f}: Background task finished (Total: {total_time:.4f}s)."
)
thread = threading.Thread(target=_apply_and_reload_task_thread)
thread.daemon = True
thread.start()
print("Configuration apply/reload task started in background.")
def _update_face_image_widget(self, icon_path):
try:
if self.face_image and self.face_image.get_window():
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(icon_path, 64, 64)
self.face_image.set_from_pixbuf(pixbuf)
except Exception as e:
print(f"Error reloading face icon preview: {e}")
if self.face_image and self.face_image.get_window():
self.face_image.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG)
return GLib.SOURCE_REMOVE
def on_reset(self, _widget):
dialog = Gtk.MessageDialog(
transient_for=self.get_toplevel(),
flags=0,
message_type=Gtk.MessageType.QUESTION,
buttons=Gtk.ButtonsType.YES_NO,
text="Reset all settings to defaults?",
)
dialog.format_secondary_text(
"This will reset all keybindings and appearance settings to their default values."
)
response = dialog.run()
dialog.hide() # Hide the dialog first
dialog.destroy() # Then destroy it
# Process any pending events
while Gtk.events_pending():
Gtk.main_iteration()
if response == Gtk.ResponseType.YES:
from . import utils
from .constants import DEFAULTS
utils.bind_vars.clear()
utils.bind_vars.update(DEFAULTS.copy())
# Reset all tabs to defaults
self.key_bindings_tab.reset_to_defaults(utils.bind_vars)
self.appearance_tab.reset_to_defaults(utils.bind_vars)
self.system_tab.reset_to_defaults(utils.bind_vars)
self._update_panel_position_sensitivity()
print("Settings reset to defaults.")
def on_close(self, _widget):
if self.application:
self.application.quit()
else:
self.destroy()