415 lines
17 KiB
Python
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.")
|