diff --git a/.gitignore b/.gitignore index 2566ca7..265fac5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ logs/* secrets/**/*.* # SHA256 hashes of the encrypted secrets -*.sha256 \ No newline at end of file +*.sha256 + +# python cache +**/__pycache__/ \ No newline at end of file diff --git a/bin/actions/auto-start.py b/bin/actions/auto-start.py new file mode 100755 index 0000000..0248d01 --- /dev/null +++ b/bin/actions/auto-start.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +import os +import sys +import time +import subprocess + +# Import helper functions +sys.path.append(os.path.join(os.path.expanduser("~/.dotfiles"), "bin")) +from helpers.functions import printfe, run_command + +def check_command_exists(command): + """Check if a command is available in the system""" + try: + subprocess.run(["which", command], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + return True + except subprocess.CalledProcessError: + return False + +def list_screen_sessions(): + """List all screen sessions""" + success, output = run_command(["screen", "-ls"]) + return output + +def wipe_dead_sessions(): + """Check and clean up dead screen sessions""" + screen_list = list_screen_sessions() + if "Dead" in screen_list: + print("Found dead sessions, cleaning up...") + run_command(["screen", "-wipe"]) + +def is_app_running(app_name): + """Check if an app is already running in a screen session""" + screen_list = list_screen_sessions() + return app_name in screen_list + +def start_app(app_name, command): + """Start an application in a screen session""" + printfe("green", f"Starting {app_name} with command: {command}...") + run_command(["screen", "-dmS", app_name] + command.split()) + time.sleep(1) # Give it a moment to start + +def main(): + # Define dictionary with app_name => command mapping + apps = { + "vesktop": "vesktop", + "ktailctl": "flatpak run org.fkoehler.KTailctl", + "ulauncher": "ulauncher --no-window-shadow --hide-window" + } + + # Clean up dead sessions if any + wipe_dead_sessions() + + print("Starting auto-start applications...") + for app_name, command in apps.items(): + # Get the binary name (first part of the command) + command_binary = command.split()[0] + + # Check if the command exists + if check_command_exists(command_binary): + # Check if the app is already running + if is_app_running(app_name): + printfe("yellow", f"{app_name} is already running. Skipping...") + continue + + # Start the application + start_app(app_name, command) + + # Display screen sessions + print(list_screen_sessions()) + + return 0 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bin/actions/auto-start.sh b/bin/actions/auto-start.sh deleted file mode 100755 index 651a0b8..0000000 --- a/bin/actions/auto-start.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -source $DOTFILES_PATH/bin/helpers/functions.sh - -# Define associative array with app_name => command mapping -declare -A apps=( - ["vesktop"]="vesktop" - ["ktailctl"]="flatpak run org.fkoehler.KTailctl" - ["ulauncher"]="ulauncher --no-window-shadow --hide-window" -) - -# check if screen has any dead sessions -if screen -list | grep -q "Dead"; then - screen -wipe -fi - -echo "Starting auto-start applications..." -for app_name in "${!apps[@]}"; do - command="${apps[$app_name]}" - command_binary=$(echo $command | awk '{print $1}') - - if [ -x "$(command -v $command_binary)" ]; then - if screen -list | grep -q $app_name; then - printfe "%s\n" "yellow" "$app_name is already running. Skipping..." - continue - fi - - printfe "%s\n" "green" "Starting $app_name with command: $command..." - screen -dmS $app_name $command - sleep 1 - fi -done - -screen -ls diff --git a/bin/actions/hello.py b/bin/actions/hello.py new file mode 100755 index 0000000..d279ada --- /dev/null +++ b/bin/actions/hello.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Import helper functions +sys.path.append(os.path.join(os.path.expanduser("~/.dotfiles"), "bin")) +from helpers.functions import printfe, logo, _rainbow_color + +def welcome(): + """Display welcome message with hostname and username""" + print() + + # Get hostname and username + hostname = os.uname().nodename + username = os.environ.get("USER", os.environ.get("USERNAME", "user")) + + print("\033[36mYou're logged in on [", end="") + print(_rainbow_color(hostname), end="") + print("\033[36m] as [", end="") + print(_rainbow_color(username), end="") + print("\033[36m]\033[0m") + +def main(): + logo(continue_after=True) + welcome() + return 0 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bin/actions/hello.sh b/bin/actions/hello.sh deleted file mode 100755 index 366d596..0000000 --- a/bin/actions/hello.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -source $HOMEsource $DOTFILES_PATH/bin/helpers/functions.sh - -welcome() { - echo - tput setaf 6 - printf "You're logged in on [" - printf $HOSTNAME | lolcat - tput setaf 6 - printf "] as " - printf "[" - printf $USER | lolcat - tput setaf 6 - printf "]\n" - tput sgr0 -} - -logo continue -welcome diff --git a/bin/actions/help.py b/bin/actions/help.py new file mode 100755 index 0000000..f0c4401 --- /dev/null +++ b/bin/actions/help.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + +import os +import sys + +# Import helper functions +sys.path.append(os.path.join(os.path.expanduser("~/.dotfiles"), "bin")) +from helpers.functions import printfe, println, logo + +def main(): + # Print logo + logo(continue_after=True) + + # Print help + dotfiles_path = os.environ.get("DOTFILES_PATH", os.path.expanduser("~/.dotfiles")) + try: + with open(f"{dotfiles_path}/bin/resources/help.txt", "r") as f: + help_text = f.read() + print(help_text) + except Exception as e: + printfe("red", f"Error reading help file: {e}") + return 1 + + println(" ", "cyan") + return 0 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bin/actions/help.sh b/bin/actions/help.sh deleted file mode 100755 index c8d7b65..0000000 --- a/bin/actions/help.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -source $DOTFILES_PATH/bin/helpers/functions.sh - -# Print logo -logo - -# Print help -cat $DOTFILES_PATH/bin/resources/help.txt -println " " "cyan" diff --git a/bin/actions/secrets.py b/bin/actions/secrets.py new file mode 100755 index 0000000..819a1ad --- /dev/null +++ b/bin/actions/secrets.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 + +import os +import sys +import subprocess +import hashlib +import glob + +# Import helper functions +sys.path.append(os.path.join(os.path.expanduser("~/.dotfiles"), "bin")) +from helpers.functions import printfe, run_command + +def is_wsl(): + """Check if running under WSL""" + try: + with open('/proc/version', 'r') as f: + return 'microsoft' in f.read().lower() + except: + return False + +def get_password(): + """Get password from 1Password""" + # Choose the appropriate op command based on WSL status + op_cmd = "op.exe" if is_wsl() else "op" + + # Try to get the password + success, output = run_command([op_cmd, "item", "get", "Dotfiles Secrets", "--fields", "password"]) + + if not success: + printfe("red", "Failed to fetch password from 1Password.") + return None + + # Check if we need to use a token + if "use 'op item get" in output: + # Extract the token + token = output.split("use 'op item get ")[1].split(" --")[0] + printfe("cyan", f"Got fetch token: {token}") + + # Use the token to get the actual password + success, password = run_command( + [op_cmd, "item", "get", token, "--reveal", "--fields", "password"] + ) + if not success: + return None + return password + else: + # We already got the password + return output + +def prompt_for_password(): + """Ask for password manually""" + import getpass + printfe("cyan", "Enter the password manually: ") + password = getpass.getpass("") + + if not password: + printfe("red", "Password cannot be empty.") + sys.exit(1) + + printfe("green", "Password entered successfully.") + return password + +def calculate_checksum(file_path): + """Calculate SHA256 checksum of a file""" + sha256_hash = hashlib.sha256() + with open(file_path, "rb") as f: + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + return sha256_hash.hexdigest() + +def encrypt_folder(folder_path, password): + """Recursively encrypt files in a folder""" + for item in glob.glob(os.path.join(folder_path, "*")): + # Skip .gpg and .sha256 files + if item.endswith(".gpg") or item.endswith(".sha256"): + continue + + # Handle directories recursively + if os.path.isdir(item): + encrypt_folder(item, password) + continue + + # Calculate current checksum + current_checksum = calculate_checksum(item) + checksum_file = f"{item}.sha256" + + # Check if file changed since last encryption + if os.path.exists(checksum_file): + with open(checksum_file, 'r') as f: + previous_checksum = f.read().strip() + + if current_checksum == previous_checksum: + continue + + # Remove existing .gpg file if it exists + gpg_file = f"{item}.gpg" + if os.path.exists(gpg_file): + os.remove(gpg_file) + + # Encrypt the file + printfe("cyan", f"Encrypting {item}...") + cmd = [ + "gpg", "--quiet", "--batch", "--yes", "--symmetric", + "--cipher-algo", "AES256", "--armor", + "--passphrase", password, + "--output", gpg_file, item + ] + + success, _ = run_command(cmd) + if success: + printfe("cyan", f"Staging {item} for commit...") + run_command(["git", "add", "-f", gpg_file]) + + # Update checksum file + with open(checksum_file, 'w') as f: + f.write(current_checksum) + else: + printfe("red", f"Failed to encrypt {item}") + +def decrypt_folder(folder_path, password): + """Recursively decrypt files in a folder""" + for item in glob.glob(os.path.join(folder_path, "*")): + # Handle .gpg files + if item.endswith(".gpg"): + output_file = item[:-4] # Remove .gpg extension + printfe("cyan", f"Decrypting {item}...") + + cmd = [ + "gpg", "--quiet", "--batch", "--yes", "--decrypt", + "--passphrase", password, + "--output", output_file, item + ] + + success, _ = run_command(cmd) + if not success: + printfe("red", f"Failed to decrypt {item}") + + # Process directories recursively + elif os.path.isdir(item): + printfe("cyan", f"Decrypting folder {item}...") + decrypt_folder(item, password) + +def main(): + if len(sys.argv) != 2 or sys.argv[1] not in ["encrypt", "decrypt"]: + printfe("red", "Usage: secrets.py [encrypt|decrypt]") + return 1 + + # Get the dotfiles path + dotfiles_path = os.environ.get("DOTFILES_PATH", os.path.expanduser("~/.dotfiles")) + secrets_path = os.path.join(dotfiles_path, "secrets") + + # Get the password + password = get_password() + if not password: + password = prompt_for_password() + + # Perform the requested action + if sys.argv[1] == "encrypt": + printfe("cyan", "Encrypting secrets...") + encrypt_folder(secrets_path, password) + else: # decrypt + printfe("cyan", "Decrypting secrets...") + decrypt_folder(secrets_path, password) + + return 0 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bin/actions/secrets.sh b/bin/actions/secrets.sh deleted file mode 100755 index 30eaf01..0000000 --- a/bin/actions/secrets.sh +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env bash - -source $DOTFILES_PATH/bin/helpers/functions.sh - -if is_wsl; then - output=$(op.exe item get "Dotfiles Secrets" --fields password) -else - output=$(op item get "Dotfiles Secrets" --fields password) -fi - -# Check if command was a success -if [[ $? -ne 0 ]]; then - printfe "%s\n" "red" "Failed to fetch password from 1Password." -fi - -# In case the output does not contain use 'op item get, it means the password was fetched successfully -# Without having to reveal the password using an external command -if [[ ! $output == *"use 'op item get"* ]]; then - password=$output -else - token=$(echo "$output" | grep -oP "(?<=\[use 'op item get ).*(?= --)") - printfe "%s\n" "cyan" "Got fetch token: $token" - - if is_wsl; then - password=$(op.exe item get $token --reveal --field password) - else - password=$(op item get $token --reveal --fields password) - fi -fi - -# only continue if password isn't empty -if [[ -z "$password" ]]; then - printfe "%s\n" "red" "Something went wrong while fetching the password from 1Password." - - # Ask for manual input - printfe "%s" "cyan" "Enter the password manually: " - read -s password - echo - - if [[ -z "$password" ]]; then - printfe "%s\n" "red" "Password cannot be empty." - exit 1 - fi - - printfe "%s\n" "green" "Password entered successfully." -fi - -encrypt_folder() { - for file in $1/*; do - # Skip if the current file is a .gpg file - if [[ $file == *.gpg ]]; then - continue - fi - - # Skip if the current file is a .sha256 file - if [[ $file == *.sha256 ]]; then - continue - fi - - # If the file is a directory, call this function recursively - if [[ -d $file ]]; then - encrypt_folder $file - continue - fi - - current_checksum=$(sha256sum "$file" | awk '{ print $1 }') - checksum_file="$file.sha256" - - if [[ -f $checksum_file ]]; then - previous_checksum=$(cat $checksum_file) - - if [[ $current_checksum == $previous_checksum ]]; then - continue - fi - fi - - # If the file has an accompanying .gpg file, remove it - if [[ -f $file.gpg ]]; then - rm "$file.gpg" - fi - - printfe "%s\n" "cyan" "Encrypting $file..." - gpg --quiet --batch --yes --symmetric --cipher-algo AES256 --armor --passphrase="$password" --output "$file.gpg" "$file" - - printfe "%s\n" "cyan" "Staging $file for commit..." - git add -f "$file.gpg" - - # Update checksum file - echo $current_checksum > "$checksum_file" - done -} - -# Recursively decrypt all .gpg files under the folder specified, recursively call this function for sub folders! -# Keep the original file name minus the .gpg extension -decrypt_folder() { - for file in $1/*; do - # Skip if current file is a .gpg file - if [[ $file == *.gpg ]]; then - filename=$(basename $file .gpg) - printfe "%s\n" "cyan" "Decrypting $file..." - gpg --quiet --batch --yes --decrypt --passphrase="$password" --output $1/$filename $file - fi - - # If file is actually a folder, call this function recursively - if [[ -d $file ]]; then - printfe "%s\n" "cyan" "Decrypting folder $file..." - decrypt_folder $file - fi - done -} - -if [[ "$1" == "decrypt" ]]; then - printfe "%s\n" "cyan" "Decrypting secrets..." - decrypt_folder $DOTFILES_PATH/secrets -elif [[ "$1" == "encrypt" ]]; then - printfe "%s\n" "cyan" "Encrypting secrets..." - encrypt_folder $DOTFILES_PATH/secrets -fi diff --git a/bin/actions/update.py b/bin/actions/update.py new file mode 100755 index 0000000..85d2264 --- /dev/null +++ b/bin/actions/update.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 + +import os +import sys +import subprocess +import argparse + +# Import helper functions +sys.path.append(os.path.join(os.path.expanduser("~/.dotfiles"), "bin")) +from helpers.functions import printfe, run_command + +def help_message(): + """Print help message and exit""" + printfe("green", "Usage: upgrade.py [options]") + printfe("green", "Options:") + printfe("green", " --ha, -H Upgrade Home Manager packages.") + printfe("green", " --ansible, -A Upgrade Ansible packages.") + printfe("green", " --ansible-verbose Upgrade Ansible packages with verbose output. (-vvv)") + printfe("green", " --full-speed, -F Upgrade packages and use all available cores for compilation. (Default: 8 cores)") + printfe("green", " --help, -h Display this help message.") + return 0 + +def ensure_ansible_collections(): + """Ensure required Ansible collections are installed""" + # List of required collections that can be expanded in the future + required_collections = [ + "community.general", + ] + + printfe("cyan", "Checking for required Ansible collections...") + status, output = run_command(["ansible-galaxy", "collection", "list"], shell=False) + + if not status: + printfe("red", "Failed to list Ansible collections") + return False + + # Check each required collection and install if missing + for collection in required_collections: + if collection not in output: + printfe("yellow", f"Installing {collection} collection...") + status, install_output = run_command(["ansible-galaxy", "collection", "install", collection], shell=False) + if not status: + printfe("yellow", f"Warning: Failed to install {collection} collection: {install_output}") + printfe("yellow", f"Continuing anyway, but playbook might fail if it requires {collection}") + else: + printfe("green", f"Successfully installed {collection} collection") + + return True + +def main(): + # Parse arguments + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("--ha", "-H", action="store_true", help="Upgrade Home Manager packages") + parser.add_argument("--ansible", "-A", action="store_true", help="Upgrade Ansible packages") + parser.add_argument("--ansible-verbose", action="store_true", help="Upgrade Ansible packages with verbose output") + parser.add_argument("--full-speed", "-F", action="store_true", help="Use all available cores") + parser.add_argument("--help", "-h", action="store_true", help="Display help message") + + args = parser.parse_args() + + if args.help: + return help_message() + + # If no specific option provided, run both + if not args.ha and not args.ansible and not args.ansible_verbose: + args.ha = True + args.ansible = True + + # If ansible_verbose is set, also set ansible + if args.ansible_verbose: + args.ansible = True + + # Set cores and jobs based on full-speed flag + if args.full_speed: + import multiprocessing + cores = jobs = multiprocessing.cpu_count() + else: + cores = 8 + jobs = 1 + + printfe("cyan", f"Limiting to {cores} cores with {jobs} jobs.") + + # Home Manager update + if args.ha: + dotfiles_path = os.environ.get("DOTFILES_PATH", os.path.expanduser("~/.dotfiles")) + hostname = os.uname().nodename + + printfe("cyan", "Updating Home Manager flake...") + os.chdir(f"{dotfiles_path}/config/home-manager") + status, output = run_command(["nix", "--extra-experimental-features", "nix-command", + "--extra-experimental-features", "flakes", "flake", "update"], + shell=False) + if not status: + printfe("red", f"Failed to update Home Manager flake: {output}") + return 1 + + # Check if home-manager is installed + status, _ = run_command(["which", "home-manager"], shell=False) + if status: + printfe("cyan", "Cleaning old backup files...") + backup_file = os.path.expanduser("~/.config/mimeapps.list.backup") + if os.path.exists(backup_file): + os.remove(backup_file) + + printfe("cyan", "Upgrading Home Manager packages...") + env = os.environ.copy() + env["NIXPKGS_ALLOW_UNFREE"] = "1" + + cmd = ["home-manager", "--extra-experimental-features", "nix-command", + "--extra-experimental-features", "flakes", "switch", "-b", "backup", + f"--flake", f".#{hostname}", "--impure", "--cores", str(cores), "-j", str(jobs)] + + result = subprocess.run(cmd, env=env) + if result.returncode != 0: + printfe("red", "Failed to upgrade Home Manager packages.") + return 1 + else: + printfe("red", "Home Manager is not installed.") + return 1 + + # Ansible update + if args.ansible: + dotfiles_path = os.environ.get("DOTFILES_PATH", os.path.expanduser("~/.dotfiles")) + hostname = os.uname().nodename + username = os.environ.get("USER", os.environ.get("USERNAME", "user")) + + # Check if ansible is installed + status, _ = run_command(["which", "ansible-playbook"], shell=False) + if not status: + printfe("yellow", "Ansible is not installed, installing it with pipx...") + status, output = run_command(["pipx", "install", "--include-deps", "ansible", "ansible-lint"], shell=False) + if not status: + printfe("red", f"Failed to install Ansible: {output}") + return 1 + + # Ensure required collections are installed + if not ensure_ansible_collections(): + printfe("red", "Failed to ensure required Ansible collections are installed") + return 1 + + printfe("cyan", "Running Ansible playbook...") + ansible_cmd = [ + "ansible-playbook", + "-i", f"{dotfiles_path}/config/ansible/inventory.ini", + f"{dotfiles_path}/config/ansible/main.yml", + "--extra-vars", f"hostname={hostname}", + "--extra-vars", f"ansible_user={username}", + "--limit", hostname, + "--ask-become-pass" + ] + + if args.ansible_verbose: + ansible_cmd.append("-vvv") + + result = subprocess.run(ansible_cmd) + if result.returncode != 0: + printfe("red", "Failed to upgrade Ansible packages.") + return 1 + + return 0 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bin/actions/update.sh b/bin/actions/update.sh deleted file mode 100755 index 81e854f..0000000 --- a/bin/actions/update.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env bash -source $DOTFILES_PATH/bin/helpers/functions.sh - -help() { - printfe "%s\n" "green" "Usage: upgrade.sh [options]" - printfe "%s\n" "green" "Options:" - printfe "%s\n" "green" " --ha, -H Upgrade Home Manager packages." - printfe "%s\n" "green" " --ansible, -A Upgrade Ansible packages." - printfe "%s\n" "green" " --ansible-verbose Upgrade Ansible packages with verbose output. (-vvv)" - printfe "%s\n" "green" " --full-speed, -F Upgrade packages and use all available cores for compilation. (Default: 8 cores)" - printfe "%s\n" "green" " --help, -h Display this help message." - exit 0 -} - -while [[ "$#" -gt 0 ]]; do - case $1 in - --ha|-H) RUN_HA=true ;; - --ansible|-A) RUN_ANSIBLE=true ;; - --ansible-verbose) - RUN_ANSIBLE=true - ANSIBLE_VERBOSE=true ;; - --full-speed|-F) FULL_SPEED=true ;; - --help|-h) help ;; - *) echo "Unknown parameter passed: $1"; - help ;; - esac - shift -done - -if [[ -z "$RUN_HA" && -z "$RUN_ANSIBLE" ]]; then - RUN_HA=true - RUN_ANSIBLE=true -fi - -# Check if --full-speed flag is passed, otherwise use --cores 8 -j 1 -if [[ "$FULL_SPEED" == true ]]; then - CORES=$(nproc) - JOBS=$(nproc) -else - CORES=8 - JOBS=1 -fi - -printfe "%s\n" "cyan" "Limiting to $CORES cores with $JOBS jobs." - -if [[ "$RUN_HA" == true ]]; then - printfe "%s\n" "cyan" "Updating Home Manager flake..." - cd $DOTFILES_PATH/config/home-manager && nix --extra-experimental-features nix-command --extra-experimental-features flakes flake update - - if command -v home-manager &> /dev/null; then - printfe "%s\n" "cyan" "Cleaning old backup files..." - rm -rf $HOME/.config/mimeapps.list.backup - - printfe "%s\n" "cyan" "Upgrading Home Manager packages..." - cd $DOTFILES_PATH/config/home-manager && NIXPKGS_ALLOW_UNFREE=1 home-manager --extra-experimental-features nix-command --extra-experimental-features flakes switch -b backup --flake .#$HOSTNAME --impure --cores $CORES -j $JOBS - - if [[ $? -ne 0 ]]; then - printfe "%s\n" "red" "Failed to upgrade Home Manager packages." - exit 1 - fi - else - printfe "%s\n" "red" "Home Manager is not installed." - exit 1 - fi -fi - -if [[ "$RUN_ANSIBLE" == true ]]; then - if ! command -v ansible-playbook &> /dev/null; then - printfe "%s\n" "yellow" "Ansible is not installed, installing it with pipx..." - pipx install --include-deps ansible ansible-lint - - if [[ $? -ne 0 ]]; then - printfe "%s\n" "red" "Failed to install Ansible." - exit 1 - fi - fi - - printfe "%s\n" "cyan" "Running Ansible playbook..." - cd $DOTFILES_PATH/config/ansible && ansible-playbook -i $DOTFILES_PATH/config/ansible/inventory.ini $DOTFILES_PATH/config/ansible/main.yml --extra-vars "hostname=$HOSTNAME" --extra-vars "ansible_user=$USER" --limit $HOSTNAME --ask-become-pass ${ANSIBLE_VERBOSE:+-vvv} - - if [[ $? -ne 0 ]]; then - printfe "%s\n" "red" "Failed to upgrade Ansible packages." - exit 1 - fi -fi diff --git a/bin/dotf b/bin/dotf index ae5d601..9668d81 100755 --- a/bin/dotf +++ b/bin/dotf @@ -1,122 +1,108 @@ -#!/usr/bin/env bash +#!/usr/bin/env python3 -# strict mode -set -euo pipefail -IFS=$'\n\t' +import os +import sys +import subprocess +from pathlib import Path # Script constants -readonly DOTFILES_ROOT="$HOME/.dotfiles" -readonly DOTFILES_BIN="$DOTFILES_ROOT/bin" +DOTFILES_ROOT = os.path.expanduser("~/.dotfiles") +DOTFILES_BIN = os.path.join(DOTFILES_ROOT, "bin") +DOTFILES_PATH = DOTFILES_ROOT # For compatibility with the original scripts -# Source helper functions -if [[ ! -f "$DOTFILES_BIN/helpers/functions.sh" ]]; then - echo "Error: Required helper functions not found" - exit 1 -fi -source "$DOTFILES_BIN/helpers/functions.sh" +# Import helper functions +sys.path.append(DOTFILES_BIN) +from helpers.functions import printfe, logo -# Command functions -update() { - local update_script="$DOTFILES_BIN/actions/update.sh" - if [[ ! -x "$update_script" ]]; then - printfe "%s\n" "red" "Error: Update script not found or not executable" +def run_script(script_path, args): + """Run an action script with the given arguments""" + if not os.path.isfile(script_path) or not os.access(script_path, os.X_OK): + printfe("red", f"Error: Script not found or not executable: {script_path}") return 1 - fi - "$update_script" $@ -} + + result = subprocess.run([script_path] + args, env={**os.environ, "DOTFILES_PATH": DOTFILES_PATH}) + return result.returncode -hello() { - local term_script="$DOTFILES_BIN/actions/hello.sh" - if [[ ! -x "$term_script" ]]; then - printfe "%s\n" "red" "Error: Terminal script not found or not executable" - return 1 - fi - "$term_script" "$@" -} +def update(args): + """Run the update action""" + return run_script(f"{DOTFILES_BIN}/actions/update.py", args) -help() { - local help_script="$DOTFILES_BIN/actions/help.sh" - if [[ ! -x "$help_script" ]]; then - printfe "%s\n" "red" "Error: Help script not found or not executable" - return 1 - fi - "$help_script" "$@" -} +def hello(args): + """Run the hello action""" + return run_script(f"{DOTFILES_BIN}/actions/hello.py", args) -secrets() { - local secrets_script="$DOTFILES_BIN/actions/secrets.sh" - if [[ ! -x "$secrets_script" ]]; then - printfe "%s\n" "red" "Error: Secrets script not found or not executable" - return 1 - fi - "$secrets_script" "$@" -} +def help(args): + """Run the help action""" + return run_script(f"{DOTFILES_BIN}/actions/help.py", args) -auto_start() { - local auto_start_script="$DOTFILES_BIN/actions/auto-start.sh" - if [[ ! -x "$auto_start_script" ]]; then - printfe "%s\n" "red" "Error: Auto-start script not found or not executable" - return 1 - fi - "$auto_start_script" "$@" -} +def secrets(args): + """Run the secrets action""" + return run_script(f"{DOTFILES_BIN}/actions/secrets.py", args) -ensure_git_hooks() { - local hooks_dir="$DOTFILES_ROOT/.git/hooks" - local target_link="$DOTFILES_BIN/actions/git" +def auto_start(args): + """Run the auto-start action""" + return run_script(f"{DOTFILES_BIN}/actions/auto-start.py", args) + +def ensure_git_hooks(): + """Ensure git hooks are correctly set up""" + hooks_dir = os.path.join(DOTFILES_ROOT, ".git/hooks") + target_link = os.path.join(DOTFILES_BIN, "actions/git") # Validate target directory exists - if [[ ! -d "$target_link" ]]; then - printfe "%s\n" "red" "Error: Git hooks source directory does not exist: $target_link" + if not os.path.isdir(target_link): + printfe("red", f"Error: Git hooks source directory does not exist: {target_link}") return 1 - fi # Handle existing symlink - if [[ -L "$hooks_dir" ]]; then - local current_link - current_link=$(readlink "$hooks_dir") - if [[ "$current_link" != "$target_link" ]]; then - printfe "%s\n" "yellow" "Incorrect git hooks symlink found. Removing and recreating..." - rm "$hooks_dir" - else + if os.path.islink(hooks_dir): + current_link = os.readlink(hooks_dir) + if current_link != target_link: + printfe("yellow", "Incorrect git hooks symlink found. Removing and recreating...") + os.remove(hooks_dir) + else: return 0 - fi - fi # Handle existing directory - if [[ -d "$hooks_dir" ]]; then - printfe "%s\n" "yellow" "Removing existing hooks directory..." - rm -rf "$hooks_dir" - fi + if os.path.isdir(hooks_dir) and not os.path.islink(hooks_dir): + printfe("yellow", "Removing existing hooks directory...") + import shutil + shutil.rmtree(hooks_dir) # Create new symlink - if ln -s "$target_link" "$hooks_dir"; then - printfe "%s\n" "green" "Git hooks successfully configured!" - else - printfe "%s\n" "red" "Failed to create git hooks symlink" + try: + os.symlink(target_link, hooks_dir) + printfe("green", "Git hooks successfully configured!") + return 0 + except Exception as e: + printfe("red", f"Failed to create git hooks symlink: {e}") return 1 - fi -} -main() { +def main(): # Ensure we're in the correct directory - if [[ ! -d "$DOTFILES_ROOT" ]]; then - printfe "%s\n" "red" "Error: Dotfiles directory not found" - exit 1 - fi + if not os.path.isdir(DOTFILES_ROOT): + printfe("red", "Error: Dotfiles directory not found") + return 1 # Setup git hooks - ensure_git_hooks || exit 1 + if ensure_git_hooks() != 0: + return 1 # Parse commands - case "${1:-help}" in - update) shift; update "$@" ;; - help) shift; help "$@" ;; - hello) shift; hello "$@" ;; - secrets) shift; secrets "$@" ;; - auto-start) shift; auto_start "$@" ;; - *) help ;; - esac -} + command = sys.argv[1] if len(sys.argv) > 1 else "help" + args = sys.argv[2:] -main "$@" + commands = { + "update": update, + "help": help, + "hello": hello, + "secrets": secrets, + "auto-start": auto_start + } + + if command in commands: + return commands[command](args) + else: + return help([]) + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bin/helpers/functions.py b/bin/helpers/functions.py new file mode 100644 index 0000000..89bdef9 --- /dev/null +++ b/bin/helpers/functions.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 + +import os +import sys +import subprocess +import math +import random +try: + import pyfiglet +except ImportError: + pyfiglet = None + +# Color codes for terminal output +COLORS = { + "black": "\033[0;30m", + "red": "\033[0;31m", + "green": "\033[0;32m", + "yellow": "\033[0;33m", + "blue": "\033[0;34m", + "purple": "\033[0;35m", + "cyan": "\033[0;36m", + "white": "\033[0;37m", + "reset": "\033[0m" +} + +def printfe(color, message): + """Print a formatted message with the specified color""" + color_code = COLORS.get(color.lower(), COLORS["reset"]) + print(f"{color_code}{message}{COLORS['reset']}") + +def println(message, color=None): + """Print a line with optional color""" + if color: + printfe(color, message) + else: + print(message) + +def _rainbow_color(text, freq=0.1, offset=0): + """Apply rainbow colors to text similar to lolcat""" + colored_text = "" + for i, char in enumerate(text): + if char.strip(): # Only color non-whitespace characters + # Calculate RGB values using sine waves with phase shifts + r = int(127 * math.sin(freq * i + offset + 0) + 128) + g = int(127 * math.sin(freq * i + offset + 2 * math.pi / 3) + 128) + b = int(127 * math.sin(freq * i + offset + 4 * math.pi / 3) + 128) + + # Apply the RGB color to the character + colored_text += f"\033[38;2;{r};{g};{b}m{char}\033[0m" + else: + colored_text += char + + return colored_text + +def logo(continue_after=False): + """Display the dotfiles logo""" + try: + # Try to read logo file first for backward compatibility + logo_path = f"{os.environ.get('DOTFILES_PATH', os.path.expanduser('~/.dotfiles'))}/bin/resources/logo.txt" + if os.path.exists(logo_path): + with open(logo_path, "r") as f: + logo_text = f.read() + print(logo_text) + elif pyfiglet: + # Generate ASCII art with pyfiglet and rainbow colors + ascii_art = pyfiglet.figlet_format("Menno's Dotfiles", font='slant') + print("\n") # Add some space before the logo + + # Use a random offset to vary the rainbow start position + random_offset = random.random() * 2 * math.pi + line_offset = 0 + + for line in ascii_art.splitlines(): + # Add a little variation to each line + print(_rainbow_color(line, offset=random_offset + line_offset)) + line_offset += 0.1 + + print("\n") # Add some space after the logo + else: + # Fallback if pyfiglet is not available + printfe("yellow", "\n\n *** Menno's Dotfiles ***\n\n") + printfe("cyan", " Note: Install pyfiglet for better logo display") + printfe("cyan", " (pip install pyfiglet)\n") + + if not continue_after: + sys.exit(0) + except Exception as e: + printfe("red", f"Error displaying logo: {e}") + +def run_command(command, shell=False): + """Run a shell command and return the result""" + try: + result = subprocess.run(command, shell=shell, check=True, text=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + return True, result.stdout.strip() + except subprocess.CalledProcessError as e: + return False, e.stderr.strip() diff --git a/bin/helpers/functions.sh b/bin/helpers/functions.sh deleted file mode 100755 index 26c80ca..0000000 --- a/bin/helpers/functions.sh +++ /dev/null @@ -1,262 +0,0 @@ -#!/usr/bin/env bash - -#Color print function, usage: println "message" "color" -println() { - color=$2 - printfe "%s\n" $color "$1" -} - -is_wsl() { - if [ -f "/proc/sys/fs/binfmt_misc/WSLInterop" ]; then - return 0 - else - return 1 - fi -} - -logo() { - echo "Menno's Dotfiles" | figlet | lolcat - - if [[ $(trash-list | wc -l) -gt 0 ]]; then - printfe "%s" "yellow" "[!] $(trash-list | wc -l | tr -d ' ') file(s) in trash - " - fi - - # Print if repo is dirty and the count of untracked files, modified files and staged files - if [[ $(git -C $DOTFILES_PATH status --porcelain) ]]; then - printfe "%s" "yellow" "dotfiles is dirty " - printfe "%s" "red" "[$(git -C $DOTFILES_PATH status --porcelain | grep -c '^??')] untracked " - printfe "%s" "yellow" "[$(git -C $DOTFILES_PATH status --porcelain | grep -c '^ M')] modified " - printfe "%s" "green" "[$(git -C $DOTFILES_PATH status --porcelain | grep -c '^M ')] staged " - fi - - printfe "%s" "blue" "[$(git -C $DOTFILES_PATH rev-parse --short HEAD)] " - if [[ $(git -C $DOTFILES_PATH log origin/master..HEAD) ]]; then - printfe "%s" "yellow" "[!] You have $(git -C $DOTFILES_PATH log origin/master..HEAD --oneline | wc -l | tr -d ' ') commit(s) to push" - fi - - println "" "normal" -} - -# print colored with printf (args: format, color, message ...) -printfe() { - format=$1 - color=$2 - shift 2 - - red=$(tput setaf 1) - green=$(tput setaf 2) - yellow=$(tput setaf 3) - blue=$(tput setaf 4) - magenta=$(tput setaf 5) - cyan=$(tput setaf 6) - normal=$(tput sgr0) - - case $color in - "red") - color=$red - ;; - "green") - color=$green - ;; - "yellow") - color=$yellow - ;; - "blue") - color=$blue - ;; - "magenta") - color=$magenta - ;; - "cyan") - color=$cyan - ;; - *) - color=$normal - ;; - esac - - printf "$color$format$normal" "$@" -} - -ensure_package_installed() { - if ! command -v $1 &>/dev/null; then - println "$1 is not installed. Please install it." "red" - exit 1 - fi - println " - $1 is available." "green" -} - -ensure_sudo_privileges() { - if sudo -n true 2>/dev/null; then - return - else - println "$1" "yellow" - sudo true - fi -} - -function exesudo () -{ - ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## - # - # LOCAL VARIABLES: - # - ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## - - # - # I use underscores to remember it's been passed - local _funcname_="$1" - - local params=( "$@" ) ## array containing all params passed here - local tmpfile="/dev/shm/$RANDOM" ## temporary file - local content ## content of the temporary file - local regex ## regular expression - - - ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## - # - # MAIN CODE: - # - ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## - - # - # WORKING ON PARAMS: - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - # - # Shift the first param (which is the name of the function) - unset params[0] ## remove first element - # params=( "${params[@]}" ) ## repack array - - - # - # WORKING ON THE TEMPORARY FILE: - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - content="#!/bin/bash\n\n" - - # - # Write the params array - content="${content}params=(\n" - - regex="\s+" - for param in "${params[@]}" - do - if [[ "$param" =~ $regex ]] - then - content="${content}\t\"${param}\"\n" - else - content="${content}\t${param}\n" - fi - done - - content="$content)\n" - echo -e "$content" > "$tmpfile" - - # - # Append the function source - echo "#$( type "$_funcname_" )" >> "$tmpfile" - - # - # Append the call to the function - echo -e "\n$_funcname_ \"\${params[@]}\"\n" >> "$tmpfile" - - - # - # DONE: EXECUTE THE TEMPORARY FILE WITH SUDO - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - sudo bash "$tmpfile" - rm "$tmpfile" -} - -resolve_path() { - echo "$(cd "$(dirname "$1")"; pwd)/$(basename "$1")" -} - -check_or_make_symlink() { - source /home/menno/dotfiles/bin/helpers/functions.sh - - SOURCE="$1" - TARGET="$2" - - # Take any ~ and replace it with $HOME - SOURCE="${SOURCE/#\~/$HOME}" - TARGET="${TARGET/#\~/$HOME}" - - # Ensure the parent directory of the target exists - mkdir -p "$(dirname "$TARGET")" - - # if source doesn't exist it's likely a secret that hasn't been decrypted yet - if [ ! -e "$SOURCE" ]; then - printfe "%s\n" "yellow" " - Source $SOURCE doesn't exist" - return - fi - - SOURCE=$(resolve_path "$SOURCE") - TARGET=$(resolve_path "$TARGET") - - # Check if we have permissions to create the symlink - if [ ! -w "$(dirname "$TARGET")" ]; then - # Check if link exists - if [ -L "$TARGET" ]; then - # Check if it points to the correct location - if [ "$(readlink "$TARGET")" != "$SOURCE" ]; then - exesudo check_or_make_symlink "$SOURCE" "$TARGET" - return - fi - else - # Link doesn't exist but we don't have permissions to create it, so we should try to create it with sudosudo - exesudo check_or_make_symlink "$SOURCE" "$TARGET" - fi - return - fi - - # If target is already a symlink, we should check if it points to the correct location - if [ -L "$TARGET" ]; then - if [ "$(readlink "$TARGET")" != "$SOURCE" ]; then - printfe "%s\n" "yellow" " - Symlink $TARGET exists but points to the wrong location" - printfe "%s\n" "yellow" " Expected: $SOURCE" - printfe "%s\n" "yellow" " Actual: $(readlink "$TARGET")" - printfe "%s\n" "yellow" " Fixing symlink" - rm "$TARGET" - mkdir -p "$(dirname "$TARGET")" - ln -s "$SOURCE" "$TARGET" - printfe "%s\n" "green" " Created symlink $TARGET -> $SOURCE" - return - fi - fi - - # If target is a file and it's not a symlink, we should back it up - if [ -f "$TARGET" ] && [ ! -L "$TARGET" ]; then - printfe "%s\n" "yellow" " - File $TARGET exists, backing up and creating symlink" - mv "$TARGET" "$TARGET.bak" - fi - - # If the target is already a symlink, and it points to the correct location, we should return and be happy - if [ -L "$TARGET" ]; then - printfe "%s" "green" " - OK: " - printfe "%-30s" "blue" "$SOURCE" - printfe "%s" "cyan" " -> " - printfe "%-30s\n" "blue" "$TARGET" - return - fi - - # Create the symlink - mkdir -p "$(dirname "$TARGET")" - ln -s "$SOURCE" "$TARGET" - - # Check if the symlink was created successfully - if [ ! -L "$TARGET" ]; then - printfe "%s\n" "red" " - Failed to create symlink $TARGET -> $SOURCE" - return - fi - - printfe "%s" "green" " - Added new symlink: " - printfe "%-30s" "blue" "$SOURCE" - printfe "%s" "cyan" " -> " - printfe "%-30s\n" "blue" "$TARGET" -} - -clear_line() { - echo -en "\r" -} \ No newline at end of file diff --git a/config/ansible/main.yml b/config/ansible/main.yml index 54a5fdf..173ac05 100644 --- a/config/ansible/main.yml +++ b/config/ansible/main.yml @@ -2,21 +2,6 @@ hosts: all gather_facts: true - pre_tasks: - - name: Check if community.general collection is installed - ansible.builtin.command: ansible-galaxy collection list community.general - register: collection_check - changed_when: false - ignore_errors: true - delegate_to: localhost - - - name: Install required collections - ansible.builtin.command: ansible-galaxy collection install -r "{{ playbook_dir }}/requirements.yml" - delegate_to: localhost - when: collection_check.rc != 0 or "community.general" not in collection_check.stdout - changed_when: collection_check.rc != 0 or "community.general" not in collection_check.stdout - run_once: true - tasks: - name: Include global tasks ansible.builtin.import_tasks: tasks/global/global.yml diff --git a/config/ansible/tasks/global/rust.yml b/config/ansible/tasks/global/rust.yml index ecdb34d..e89fec0 100644 --- a/config/ansible/tasks/global/rust.yml +++ b/config/ansible/tasks/global/rust.yml @@ -18,5 +18,6 @@ set -o pipefail /tmp/rustup.sh -y args: + executable: /bin/bash creates: ~/.cargo/bin/rustc when: rust_check.rc != 0 diff --git a/setup.sh b/setup.sh index 9a5a8a9..03db50d 100755 --- a/setup.sh +++ b/setup.sh @@ -414,11 +414,11 @@ main() { export PATH=$PATH:$DOTFILES_PATH/bin # Create new .bashrc with exports for initial `dotf update` command - cat << 'EOF' >> $HOME/.bashrc + cat << EOF >> $HOME/.bashrc export NIXPKGS_ALLOW_INSECURE=1 -export DOTFILES_PATH=$DOTFILES_PATH -export PATH=$PATH:$DOTFILES_PATH/bin -export PATH=$PATH:$HOME/.local/bin +export DOTFILES_PATH=${DOTFILES_PATH} +export PATH=\$PATH:\$DOTFILES_PATH/bin +export PATH=\$PATH:\$HOME/.local/bin EOF cp $HOME/.bashrc $HOME/.profile