Files
Modus/config/settings/utils.py
T
2025-07-21 00:55:51 +05:45

415 lines
17 KiB
Python

import json
import os
import shutil
import subprocess
import time
import gi
import toml
from fabric.utils.helpers import exec_shell_command_async
from config.data import (
APP_NAME,
APP_NAME_CAP,
)
from config.settings import constants as settings_constants
gi.require_version("Gtk", "3.0")
# Global variable to store binding variables, managed by this module
bind_vars = {}
def deep_update(target: dict, update: dict) -> dict:
"""
Recursively update a nested dictionary with values from another dictionary.
Modifies target in-place.
"""
for key, value in update.items():
if isinstance(value, dict) and key in target and isinstance(target[key], dict):
# If the value is a dictionary and the key already exists in target as a dictionary,
# then update recursively.
deep_update(target[key], value)
else:
# Otherwise, simply set/overwrite the value.
target[key] = value
return target # Although it modifies in-place, returning it is a common convention
def ensure_matugen_config():
"""
Ensure that the matugen configuration file exists and is updated
with the expected settings.
"""
expected_config = {
"config": {
"reload_apps": True,
"wallpaper": {
"command": "swww",
"arguments": [
"img",
"-t",
"outer",
"--transition-duration",
"1.5",
"--transition-step",
"255",
"--transition-fps",
"60",
"-f",
"Nearest",
],
"set": True,
},
"custom_colors": {
"red": {"color": "#FF0000", "blend": True},
"green": {"color": "#00FF00", "blend": True},
"yellow": {"color": "#FFFF00", "blend": True},
"blue": {"color": "#0000FF", "blend": True},
"magenta": {"color": "#FF00FF", "blend": True},
"cyan": {"color": "#00FFFF", "blend": True},
"white": {"color": "#FFFFFF", "blend": True},
},
},
"templates": {
"hyprland": {
"input_path": f"~/.config/{APP_NAME_CAP}/config/matugen/templates/hyprland-colors.conf",
"output_path": f"~/.config/{APP_NAME_CAP}/config/hypr/colors.conf",
},
f"{APP_NAME}": {
"input_path": f"~/.config/{APP_NAME_CAP}/config/matugen/templates/{APP_NAME}.css",
"output_path": f"~/.config/{APP_NAME_CAP}/styles/colors.css",
"post_hook": f"fabric-cli exec {APP_NAME} 'app.set_css()' &",
},
},
}
config_path = os.path.expanduser("~/.config/matugen/config.toml")
os.makedirs(os.path.dirname(config_path), exist_ok=True)
existing_config = {}
if os.path.exists(config_path):
try:
with open(config_path, "r") as f:
existing_config = toml.load(f)
shutil.copyfile(config_path, config_path + ".bak")
except toml.TomlDecodeError:
print(
f"Warning: Could not decode TOML from {
config_path
}. A new default config will be created."
)
existing_config = {} # Reset if corrupted
except Exception as e:
print(f"Error reading or backing up {config_path}: {e}")
# existing_config could be partially loaded or empty.
# Continue to try merging with defaults.
# We use a copy of existing_config for deep_update if we don't want to modify it directly
# or ensure that deep_update doesn't do it if not desired.
# The current implementation of deep_update modifies 'target'.
# To be safer, we can pass a copy if existing_config should not change.
# merged_config = deep_update(existing_config.copy(), expected_config)
# Or if existing_config can be modified:
# existing_config is modified in-place
merged_config = deep_update(existing_config, expected_config)
try:
with open(config_path, "w") as f:
toml.dump(merged_config, f)
except Exception as e:
print(f"Error writing matugen config to {config_path}: {e}")
current_wall = os.path.expanduser("~/.current.wall")
hypr_colors = os.path.expanduser(
f"~/.config/{APP_NAME_CAP}/config/hypr/colors.conf"
)
css_colors = os.path.expanduser(f"~/.config/{APP_NAME_CAP}/styles/colors.css")
if (
not os.path.exists(current_wall)
or not os.path.exists(hypr_colors)
or not os.path.exists(css_colors)
):
os.makedirs(os.path.dirname(hypr_colors), exist_ok=True)
os.makedirs(os.path.dirname(css_colors), exist_ok=True)
image_path = ""
if not os.path.exists(current_wall):
example_wallpaper_path = os.path.expanduser(
f"~/.config/{APP_NAME_CAP}/assets/wallpapers_example/example-1.jpg"
)
if os.path.exists(example_wallpaper_path):
try:
# If it already exists (possibly a broken link or regular file), remove and re-link
# lexists to not follow the link if it is one
if os.path.lexists(current_wall):
os.remove(current_wall)
os.symlink(example_wallpaper_path, current_wall)
image_path = example_wallpaper_path
except Exception as e:
print(f"Error creating symlink for wallpaper: {e}")
else:
image_path = (
os.path.realpath(current_wall)
if os.path.islink(current_wall)
else current_wall
)
if image_path and os.path.exists(image_path):
print(f"Generating color theme from wallpaper: {image_path}")
try:
matugen_cmd = f"matugen image '{image_path}'"
exec_shell_command_async(matugen_cmd)
print("Matugen color theme generation initiated.")
except FileNotFoundError:
print("Error: matugen command not found. Please install matugen.")
except Exception as e:
print(f"Error initiating matugen: {e}")
elif not image_path:
print(
"Warning: No wallpaper path determined to generate matugen theme from."
)
else: # image_path exists but the file doesn't
print(
f"Warning: Wallpaper at {
image_path
} not found. Cannot generate matugen theme."
)
def load_bind_vars():
"""
Load saved key binding variables from JSON, if available.
Populates the global `bind_vars` in-place.
"""
global bind_vars # Necessary to modify the global bind_vars object
# 1. Clear the existing bind_vars dictionary.
bind_vars.clear()
# 2. Update it with a copy of DEFAULTS.
# Use .copy() to not accidentally modify DEFAULTS
bind_vars.update(settings_constants.DEFAULTS.copy())
config_json = os.path.expanduser(
f"~/.config/{APP_NAME_CAP}/config/assets/config.json"
)
if os.path.exists(config_json):
try:
with open(config_json, "r") as f:
saved_vars = json.load(f)
# 3. Use deep_update to merge saved_vars into the existing bind_vars.
deep_update(bind_vars, saved_vars)
# The logic to ensure the structure of nested dictionaries
# like 'metrics_visible' and 'metrics_small_visible'
# should operate on the already updated 'bind_vars'.
for vis_key in ["metrics_visible"]:
# Ensure that the key exists in DEFAULTS as a structure reference
if vis_key in settings_constants.DEFAULTS:
default_sub_dict = settings_constants.DEFAULTS[vis_key]
# If the key is not in bind_vars or is not a dictionary after deep_update,
# restore it from a copy of DEFAULTS for that key.
if not isinstance(bind_vars.get(vis_key), dict):
bind_vars[vis_key] = default_sub_dict.copy()
else:
# If it is a dictionary, ensure that all sub-keys from DEFAULTS are present.
current_sub_dict = bind_vars[vis_key]
for m_key, m_val in default_sub_dict.items():
if m_key not in current_sub_dict:
current_sub_dict[m_key] = m_val
except json.JSONDecodeError:
print(
f"Warning: Could not decode JSON from {
config_json
}. Using defaults (already initialized)."
)
# bind_vars is already populated with DEFAULTS, no additional action needed here.
except Exception as e:
print(
f"Error loading config from {config_json}: {
e
}. Using defaults (already initialized)."
)
# bind_vars is already populated with DEFAULTS.
# else:
# If config_json doesn't exist, bind_vars is already populated with DEFAULTS.
# print(f"Config file {config_json} not found. Using defaults (already initialized).")
def generate_hyprconf() -> str:
"""
Generate the Hypr configuration string using the current bind_vars.
"""
home = os.path.expanduser("~")
# Determine animation type based on bar position
dock_position = bind_vars.get("dock_position", "Bottom")
is_vertical = dock_position in ["Left", "Right"]
animation_type = "slidefadevert" if is_vertical else "slidefade"
return f"""exec-once = uwsm-app $(python {home}/.config/{APP_NAME_CAP}/main.py)
exec = pgrep -x "hypridle" > /dev/null || uwsm app -- hypridle
exec = uwsm app -- swww-daemon
exec-once = wl-paste --type text --watch cliphist store
exec-once = wl-paste --type image --watch cliphist store
$fabricSend = fabric-cli exec {APP_NAME}
$Message = notify-send "Modus" "FIRE IN THE HOLE‼️🗣️🔥🕳️" -i "{home}/.config/{APP_NAME_CAP}/assets/modus.png" -A "🗣️" -A "🔥" -A "🕳️" -a "Source Code"
# Reload {APP_NAME_CAP}
bind = {bind_vars.get("prefix_restart", "ALT SHIFT")}, {bind_vars.get("suffix_restart", "R")}, exec, killall {APP_NAME}; uwsm-app $(python {home}/.config/{APP_NAME_CAP}/main.py)
# Message
bind = {bind_vars.get("prefix_msg", "SUPER")}, {bind_vars.get("suffix_msg", "A")}, exec, $Message
# Application Switcher
bind = {bind_vars.get("prefix_application_switcher", "ALT")}, {bind_vars.get("suffix_application_switcher", "TAB")}, exec, $fabricSend 'switcher.show_switcher()'
# Kanban
bind = {bind_vars.get("prefix_kanban", "SUPER")}, {bind_vars.get("suffix_kanban", "T")}, exec, $fabricSend "launcher.show_launcher('kanban')" # Kanban
# App Launcher
bind = {bind_vars.get("prefix_launcher", "SUPER")}, {bind_vars.get("suffix_launcher", "SPACE")}, exec, $fabricSend "launcher.show_launcher()"
bind = {bind_vars.get("prefix_app_launcher", "SUPER")}, {bind_vars.get("suffix_app_launcher", "D")}, exec, $fabricSend "launcher.show_launcher('app')"
# Clipboard History
bind = {bind_vars.get("prefix_cliphist", "SUPER")}, {bind_vars.get("suffix_cliphist", "V")}, exec, $fabricSend "launcher.show_launcher('clip')"
# Wallpapers
bind = {bind_vars.get("prefix_wallpapers", "SUPER")}, {bind_vars.get("suffix_wallpapers", "W")}, exec, $fabricSend "launcher.show_launcher('wall')"
# Random Wallpaper
bind = {bind_vars.get("prefix_randwall", "SUPER")}, {bind_vars.get("suffix_randwall", "W")}, exec, $fabricSend "launcher.show_launcher('wall random', external=True)"
# Emoji Picker
bind = {bind_vars.get("prefix_emoji", "SUPER")}, {bind_vars.get("suffix_emoji", "E")}, exec, $fabricSend "launcher.show_launcher('em')"
# Power Menu
bind = {bind_vars.get("prefix_power", "SUPER")}, {bind_vars.get("suffix_power", "ESCAPE")}, exec, $fabricSend "launcher.show_launcher('power')"
# Toggle Caffeine
bind = {bind_vars.get("prefix_caffeine", "SUPER SHIFT")}, {bind_vars.get("suffix_caffeine", "M")}, exec, $fabricSend "launcher.show_launcher('caffeine on', external=True)"
# Settings
bind = {bind_vars.get("prefix_settings", "SUPER")}, {bind_vars.get("suffix_settings", "I")}, exec, uwsm-app $(python {home}/.config/{APP_NAME_CAP}/config/config.py) # Settings
# Restart Modus
bind = {bind_vars.get("prefix_restart_inspector", "SUPER CTRL ALT")}, {bind_vars.get("suffix_restart_inspector", "B")}, exec, killall {APP_NAME}; uwsm-app $(GTK_DEBUG=interactive python {home}/.config/{APP_NAME_CAP}/main.py) # Restart with inspector
# Wallpapers directory: {bind_vars.get("wallpapers_dir", "~/Modus/assets/wallpapers_example")}
source = {home}/.config/{APP_NAME_CAP}/config/hypr/colors.conf
layerrule = noanim, fabric
exec = cp $wallpaper ~/.current.wall
general {{
col.active_border = rgb($primary)
col.inactive_border = rgb($surface)
gaps_in = 2
gaps_out = 4
border_size = 2
layout = dwindle
}}
cursor {{
no_warps=true
}}
decoration {{
blur {{
enabled = yes
size = 5
passes = 3
new_optimizations = yes
contrast = 1
brightness = 1
}}
rounding = 14
shadow {{
enabled = true
range = 10
render_power = 2
color = rgba(0, 0, 0, 0.25)
}}
}}
animations {{
enabled = yes
bezier = myBezier, 0.4, 0.0, 0.2, 1.0
animation = windows, 1, 2.5, myBezier, popin 80%
animation = border, 1, 2.5, myBezier
animation = fade, 1, 2.5, myBezier
animation = workspaces, 1, 2.5, myBezier, {animation_type} 20%
}}
"""
def ensure_face_icon():
"""
Ensure the face icon exists. If not, copy the default icon.
"""
face_icon_path = os.path.expanduser("~/.face.icon")
default_icon_path = os.path.expanduser(
f"~/.config/{APP_NAME_CAP}/assets/default.png"
)
if not os.path.exists(face_icon_path) and os.path.exists(default_icon_path):
try:
shutil.copy(default_icon_path, face_icon_path)
except Exception as e:
print(f"Error copying default face icon: {e}")
def backup_and_replace(src: str, dest: str, config_name: str):
"""
Backup the existing configuration file and replace it with a new one.
"""
try:
if os.path.exists(dest):
backup_path = dest + ".bak"
# Ensure that the backup directory exists if it's different
# os.makedirs(os.path.dirname(backup_path), exist_ok=True)
shutil.copy(dest, backup_path)
print(f"{config_name} config backed up to {backup_path}")
# Ensure dest directory exists
os.makedirs(os.path.dirname(dest), exist_ok=True)
shutil.copy(src, dest)
print(f"{config_name} config replaced from {src}")
except Exception as e:
print(f"Error backing up/replacing {config_name} config: {e}")
def start_config():
"""
Run final configuration steps: ensure necessary configs, write the hyprconf, and reload.
"""
print(f"{time.time():.4f}: start_config: Ensuring matugen config...")
ensure_matugen_config()
print(f"{time.time():.4f}: start_config: Ensuring face icon...")
ensure_face_icon()
print(f"{time.time():.4f}: start_config: Generating hypr conf...")
hypr_config_dir = os.path.expanduser(f"~/.config/{APP_NAME_CAP}/config/hypr/")
os.makedirs(hypr_config_dir, exist_ok=True)
# Use APP_NAME for the .conf file name to match the corrected SOURCE_STRING
hypr_conf_path = os.path.join(hypr_config_dir, f"{APP_NAME}.conf")
try:
with open(hypr_conf_path, "w") as f:
f.write(generate_hyprconf())
print(f"Generated Hyprland config at {hypr_conf_path}")
except Exception as e:
print(f"Error writing Hyprland config: {e}")
print(f"{time.time():.4f}: start_config: Finished generating hypr conf.")
print(f"{time.time():.4f}: start_config: Initiating hyprctl reload...")
try:
# subprocess.run(["hyprctl", "reload"], check=True, capture_output=True, text=True)
# Keep async to not block
exec_shell_command_async("hyprctl reload")
print(
f"{time.time():.4f}: start_config: Hyprland configuration reload initiated."
)
except FileNotFoundError:
print("Error: hyprctl command not found. Cannot reload Hyprland.")
except (
subprocess.CalledProcessError
) as e: # If we used subprocess.run with check=True
print(
f"Error reloading Hyprland with hyprctl: {e}\nOutput:\n{e.stdout}\n{
e.stderr
}"
)
except Exception as e:
print(f"An error occurred initiating hyprctl reload: {e}")
print(f"{time.time():.4f}: start_config: Finished initiating hyprctl reload.")