diff --git a/p3/app/cli_helper.py b/p3/app/cli_helper.py index 7d9346b0..059513e0 100644 --- a/p3/app/cli_helper.py +++ b/p3/app/cli_helper.py @@ -287,6 +287,7 @@ def run_script(script_info): In developer mode, performs dry-run validation instead of execution. """ # Check if we should dry-run instead of execute + try: from .dev_mode import should_dry_run_scripts, dry_run_script if should_dry_run_scripts(): @@ -301,11 +302,19 @@ def run_script(script_info): print("-" * 50) try: - # Execute the script with bash, similar to how the GUI does it - result = subprocess.run(['bash', script_info['path']], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - universal_newlines=True) + if os.environ.get("EASY_CLI") == "1": + result = subprocess.run(['bash', script_info['path']], + stdin=sys.stdin, + stdout=sys.stdout, + stderr=sys.stderr, + check=True) + + else: + # Execute the script with bash, similar to how the GUI does it + result = subprocess.run(['bash', script_info['path']], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True) # Print the output if result.stdout: @@ -404,6 +413,7 @@ def check_ostree_deployment_cli(translations=None): return False + def print_cli_usage(): """ Print usage information for CLI mode. @@ -446,21 +456,40 @@ def run_manifest_mode(translations=None): # Parse command-line arguments manifest_path = 'manifest.txt' # Default manifest path - if len(sys.argv) > 1: - arg = sys.argv[1] - - # Check for help request - if arg in ['--help', '-h', 'help']: - print_cli_usage() - return 0 - - # Check if user wants to run update check - elif arg in ['check-updates', 'update-check', '--check-updates']: - return 1 if run_update_check_cli(translations) else 0 - - # Otherwise, treat the argument as a manifest file path - else: - manifest_path = arg + # If LT_MANIFEST != "1", it means the script is running in EASY_CLI mode. + if os.environ.get("LT_MANIFEST") != "1": + if len(sys.argv) > 2: + arg = sys.argv[2] + + # Check for help request + if arg in ['--help', '-h', 'help']: + print_cli_usage() + return 0 + + # Check if user wants to run update check + elif arg in ['check-updates', 'update-check', '--check-updates']: + return 1 if run_update_check_cli(translations) else 0 + + # Otherwise, treat the argument as a manifest file path + else: + manifest_path = arg + + else: + if len(sys.argv) > 1: + arg = sys.argv[1] + + # Check for help request + if arg in ['--help', '-h', 'help']: + print_cli_usage() + return 0 + + # Check if user wants to run update check + elif arg in ['check-updates', 'update-check', '--check-updates']: + return 1 if run_update_check_cli(translations) else 0 + + # Otherwise, treat the argument as a manifest file path + else: + manifest_path = arg print("LinuxToys CLI Manifest Mode") print("=" * 40) diff --git a/p3/app/easy_cli.py b/p3/app/easy_cli.py new file mode 100644 index 00000000..886375e3 --- /dev/null +++ b/p3/app/easy_cli.py @@ -0,0 +1,382 @@ +#!/usr/bin/env python3 + +import os +import sys +import tempfile +from .parser import get_categories, get_all_scripts_recursive +from .update_helper import get_current_version +from .cli_helper import run_manifest_mode, run_update_check_cli, find_script_by_name, run_script +from .dev_mode import is_dev_mode_enabled + +def resolve_script_dir(): + """ + Ensure SCRIPT_DIR exists based on the current file's location. + Searches parent directories until one containing 'libs' is found. + Sets SCRIPT_DIR to that absolute path. + Raises FileNotFoundError if no 'libs' directory is found. + """ + current_dir = os.path.dirname(os.path.abspath(__file__)) + + while True: + libs_path = os.path.join(current_dir, "libs") + if os.path.isdir(libs_path): + os.environ["SCRIPT_DIR"] = current_dir + return current_dir + + parent_dir = os.path.dirname(current_dir) + if parent_dir == current_dir: + break + current_dir = parent_dir + + raise FileNotFoundError(f"'libs' folder not found relative to {__file__}") + +def create_temp_file(script_path): + """ + Create a temporary script file by filtering out xdg-open calls. + """ + + # Resolve SCRIPT_DIR + script_dir = resolve_script_dir() + + # Open the original script and set to a list + with open(script_path, "r", encoding="utf-8") as f: + lines = f.readlines() + + filtered_lines = [] + for line in lines: + # Ignore lines containing "xdg-open" + if "xdg-open" in line: + continue + # Replace $SCRIPT_DIR + line = line.replace("$SCRIPT_DIR", script_dir) + if line.strip().startswith("/libs/"): + line = line.replace("/libs/", f"{script_dir}/libs/") + elif " libs/" in line: + line = line.replace(" libs/", f" {script_dir}/libs/") + filtered_lines.append(line) + + tmp_file = tempfile.NamedTemporaryFile(delete=False, mode="w", encoding="utf-8") + tmp_file.writelines(filtered_lines) + tmp_file.close() + temp_file_path = tmp_file.name + return temp_file_path + + +def easy_cli_run_script(script_info): + """ + Run a LinuxToys script in EASY_CLI mode while preventing any xdg-open calls. + """ + + # Check if dev mode is enabled and run the script + if is_dev_mode_enabled(): + return run_script(script_info) + + # Disable zenity to avoid GUI prompts during EASY_CLI execution + os.environ['DISABLE_ZENITY'] = '1' + + script_path = script_info['path'] + + # Create a temporary script file + temp_file_path = create_temp_file(script_path) + + + try: + # Execute the script using run_script + code = run_script({"name": script_info["name"], "path": temp_file_path}) + + if code != 0: + return 1 + + except KeyboardInterrupt: + # Stop execution if the user presses Ctrl+C + return 130 + except Exception as e: + print(f"✗ Error while executing the script: {e}") + return 1 + finally: + # Remove the temporary script file + os.remove(temp_file_path) + + return code + + +def confirm_action(prompt_message): + """Ask the user to confirm an action.""" + try: + response = input(f"{prompt_message} [y/N]: ").strip().lower() + if response not in ['y', 'yes']: + print("❌ Operation cancelled.") + return False + except KeyboardInterrupt: + print("\n⚠️ Operation cancelled by user.") + return False + return True + + +def execute_scripts_with_feedback(scripts_found): + """Execute each script sequentially and provide CLI feedback.""" + total = len(scripts_found) + + for index, script_info in enumerate(scripts_found, 1): + name = script_info.get("name", os.path.basename(script_info["path"])) + print(f"\n[{index}/{total}] 🚀 Running: {name}") + print("=" * 60) + + exit_code = easy_cli_run_script(script_info) + + if exit_code == 0: + print(f"✓ {name} Completed successfully.") + + elif exit_code == 1: + print(f"✗ {name} Failed with exit code: {exit_code}.") + + elif exit_code == 130: + print("\n⚠️ Execution interrupted by the user.") + break + else: + print(f"✗ {name} Failed with exit code: {exit_code}.") + # Ask the user if they want to continue with the remaining scripts + if not confirm_action("Do you want to continue with the remaining scripts?"): + print("❌ Operation cancelled.") + break + + +def scripts_install(args: list, skip_confirmation, translations): + """Handle script installation in EASY_CLI mode.""" + + # Filter out confirmation flags from the install list + install_list = [arg for arg in args if arg not in ("-y", "--yes")] + + # Check if any script was specified + if not install_list: + print("\n✗ No items specified for installation.\n") + easy_cli_help_message() + return 0 + + print("🧰 EASY CLI INSTALL MODE") + print("=" * 60) + print(f"📜 Requested scripts: {', '.join(install_list)}\n") + + scripts_found_list = [] + scripts_missing = [] + + # Search scripts by name + for script_name in install_list: + script_info = find_script_by_name(script_name, translations) + if script_info: + scripts_found_list.append(script_info) + else: + scripts_missing.append(script_name) + + # Report missing scripts + if scripts_missing: + print("⚠️ Scripts not found:") + for name in scripts_missing: + print(f" - {name}") + print() + + if not scripts_found_list: + print("✗ No valid scripts found. Aborting.") + return 0 + + # Calculate column widths for display + max_file_len = max(len(os.path.basename(s["path"])) for s in scripts_found_list) + max_name_len = max(len(s["name"]) for s in scripts_found_list) + + # Display found scripts + print(f"✅ {len(scripts_found_list)} Script(s) found and ready for execution:\n") + for script_info in scripts_found_list: + print(f" - {script_info['name']:<{max_name_len}} | {os.path.basename(script_info['path']):<{max_file_len}}") + print() + + # Ask user to confirm execution + if skip_confirmation or confirm_action("Confirm script execution?"): + execute_scripts_with_feedback(scripts_found_list) + + +def print_script_list(translations): + """Print all available scripts in a formatted list.""" + scripts = get_all_scripts(translations) + + # Calculate column widths for alignment + max_file_len = max(len(os.path.splitext(os.path.basename(s["path"]))[0]) for s in scripts) + max_name_len = max(len(s["name"]) for s in scripts) + + print(f"\nScripts found: {len(scripts)}\n") + print(f" {'SCRIPT':<{max_file_len}} {'NAME':<{max_name_len}}") + print("=" * (max_file_len + max_name_len + 4)) + + for script in sorted(scripts, key=lambda s: s["name"].lower()): + filename = os.path.splitext(os.path.basename(script["path"]))[0] + print(f" - {filename:<{max_file_len}} --> {script['name']:<{max_name_len}}") + + +def get_all_scripts(translations=None): + """Return a sorted list of all scripts.""" + scripts = [] + categories = get_categories(translations) or [] + + def add_script(name, path): + if not name or not path: + return + scripts.append({"name": name, "path": path}) + + for category in categories: + path = category.get('path') + name = category.get('name') + if not path or not name: + continue + + if category.get('is_script'): + add_script(name, path) + else: + for script in (get_all_scripts_recursive(path, translations) or []): + add_script(script.get('name'), script.get('path')) + + # Remove duplicates and sort by name + unique_scripts = { (s["name"], s["path"]) : s for s in scripts }.values() + return sorted(unique_scripts, key=lambda s: s["name"]) + + +def easy_cli_help_message(): + """Print usage information for EASY CLI mode.""" + print("LinuxToys EASY CLI Usage:") + print("=" * 60) + print("Usage:") + print(" EASY_CLI=1 linuxtoys -i [option] ...") + # print(" EASY_CLI=1 python3 run.py --install [option] ...") + print() + print("Functions:") + print(" -i, --install Install selected options") + print() + print("Install options:") + print(" -s, --script Install specified LinuxToys scripts") + # print(" -p, --package Install specified LinuxToys packages") + # print(" -f, --flatpak Install specified LinuxToys flatpaks") + print(" -l, --list List all available scripts") + print() + print("Examples:") + print(" EASY_CLI=1 linuxtoys --install --script ") + # print(" EASY_CLI=1 linuxtoys --install -p ") + # print(" EASY_CLI=1 linuxtoys --install -f ") + print() + print("Other options:") + print(" -h, --help Show this help message") + print(" -m, --manifest Enable manifest mode features") + print(" -v, --version Show version information") + print(" -y, --yes Skip confirmation prompts (recommended as the last argument)") + print() + + +# --- MAIN EASY CLI HANDLER --- +def easy_cli_handler(translations=None): + """ + Handles the EASY CLI mode for LinuxToys, parsing command-line arguments and executing actions. + + Supports: + - Installing scripts (--install -s ...) + - Listing available scripts (--install -l) + - Checking for updates (update, upgrade, --check-updates) + - Running in manifest mode (--manifest, -m) + - Displaying version (-v, --version) + - Displaying help (-h, --help) + + It also supports developer mode (-D, --DEV_MODE) and optional automatic + confirmation flags (-y, --yes) to skip prompts. + """ + + # --- Developer Mode --- + def dev_check(args): + dev_flags = ("-D", "--DEV_MODE") + found = False + + for flag in dev_flags: + while flag in args: + args.remove(flag) + found = True + + if found and not os.environ.get("DEV_MODE"): + os.environ["DEV_MODE"] = "1" + try: + from app.dev_mode import print_dev_mode_banner + print_dev_mode_banner() + except ImportError: + pass + + # --- Skip confirmation flags --- + def skip_confirmation(args): + if os.environ.get("DEV_MODE") == "1": + return True + + skip_flags = ("-y", "--yes") + found = False + for flag in skip_flags: + while flag in args: + args.remove(flag) + found = True + + return found + + args = sys.argv[1:] + + dev_check(args) + + if not args: + print("✗ No arguments provided.\n") + easy_cli_help_message() + return 0 + + if args[0] in ("-i", "--install"): + if len(args) < 2: + print("✗ Missing parameter after '-i' | '--install'.\n") + print("Use:") + print(" [-s | --script] for scripts") + # print(" [-p | --package] for packages") + # print(" [-f | --flatpak] for flatpaks") + print(" [-l | --list] list all available scripts") + return 0 + + if args[1] in ("-s", "--script", "--scripts"): + scripts_install(args[2:], skip_confirmation(args), translations) + return 0 + + # TODO : Implement instalation of pakages and flatpaks + # elif args[1] in ("-p", "--package"): # Para instalação de pacotes + # packages_install(args[2:], skip_confirmation(args), translations) + # return 0 + + # elif args[1] in ("-f", "--flatpak"): # Para instalação de flatpaks + # flatpaks_install(args[2:], skip_confirmation(args), translations) + # return 0 + + elif args[1] in ("-l", "--list"): + print_script_list(translations) + return 0 + + else: + print("✗ Invalid parameter after '-i' | '--install'.\n") + easy_cli_help_message() + return 0 + + elif args[0] in ("-l", "--list"): + print_script_list(translations) + return 0 + + elif args[0] in ("-h", "--help", "help"): + easy_cli_help_message() + return 0 + + elif args[0] in ("update", "upgrade", "check-updates", "update-check", "--check-updates"): + return 1 if run_update_check_cli(translations) else 0 + + elif args[0] in ("--manifest", "-m"): + return run_manifest_mode(translations) + + elif args[0] in ("-v", "--version"): + print(f"LinuxToys {get_current_version()}") + return 0 + + else: + print(f"\n✗ Unknown action: {args[0]} \n") + easy_cli_help_message() + return 0 diff --git a/p3/app/main.py b/p3/app/main.py index f764eabe..c6776105 100644 --- a/p3/app/main.py +++ b/p3/app/main.py @@ -8,6 +8,7 @@ from .lang_utils import load_translations, create_translator from .cli_helper import run_manifest_mode +from .easy_cli import easy_cli_handler from .update_helper import run_update_check from .kernel_update_helper import run_kernel_update_check from .compat import is_supported_system @@ -48,6 +49,13 @@ def load_css(self): _ = create_translator() # Create translator function from lang_utils def run(): + + # Check for CLI mode + if os.environ.get('EASY_CLI') == '1': + # Run in EASY_CLI + sys.exit(easy_cli_handler(translations)) + + # Check for CLI manifest mode if os.environ.get('LT_MANIFEST') == '1': # Run in CLI mode using manifest.txt diff --git a/p3/libs/linuxtoys.lib b/p3/libs/linuxtoys.lib index ca27ed09..7c524b5a 100644 --- a/p3/libs/linuxtoys.lib +++ b/p3/libs/linuxtoys.lib @@ -5,6 +5,40 @@ export SUDO_ASKPASS="$SCRIPT_DIR/libs/zpass.sh" # sudo request sudo_rq () { + # CLI mode (no zenity) - ask password up to 3 times + if [[ "$DISABLE_ZENITY" == "1" ]]; then + # check if sudo is already authorized + if sudo -n true 2>/dev/null; then + # already validated + [ -n "$LINUXTOYS_CHECKLIST" ] && touch /tmp/linuxtoys_sudo_validated + return 0 + fi + + local max_attempts=3 + local attempts=0 + + while [ $attempts -lt $max_attempts ]; do + # prompt for password silently + printf "Password: " + read -s _pass + printf "\n" + + # validate password + if echo "${_pass}" | sudo -S -v >/dev/null 2>&1; then + # successfully validated + [ -n "$LINUXTOYS_CHECKLIST" ] && touch /tmp/linuxtoys_sudo_validated + return 0 + else + attempts=$((attempts + 1)) + echo "❌ Wrong password. Attempts: ${attempts}/${max_attempts}." + fi + done + + # reached max attempts + fatal "❌ Wrong password or sudo failed (max attempts reached)." + fi + + if [ -f /tmp/linuxtoys_sudo_validated ]; then return 0 fi @@ -27,20 +61,38 @@ sudo_rq () { # zenity libs zeninf () { + if [[ "$DISABLE_ZENITY" == "1" ]]; then + echo + echo "$1" + return 0 + fi zenity --info --text "$1" --width 360 --height 300 return 0 } zenwrn () { + if [[ "$DISABLE_ZENITY" == "1" ]]; then + echo + echo "$1" + return 0 + fi zenity --warning --text "$1" --width 360 --height 300 return 0 } ## error handlers fatal() { + if [[ "$DISABLE_ZENITY" == "1" ]]; then + echo "$1" + exit 1 + fi zenity --error --title "Fatal Error" --text "$1" --width 360 --height 300 exit 1 } nonfatal() { + if [[ "$DISABLE_ZENITY" == "1" ]]; then + echo "$1" + exit 1 + fi zenity --error --title "Error" --text "$1" --width 360 --height 300 return 1 } diff --git a/p3/scripts/devs/docker.sh b/p3/scripts/devs/docker.sh index fb893607..359a368e 100755 --- a/p3/scripts/devs/docker.sh +++ b/p3/scripts/devs/docker.sh @@ -66,7 +66,8 @@ docker_in () { # install docker sudo systemctl enable --now docker.socket sleep 2 } -if zenity --question --title "Docker" --text "This will install Docker Engine. Proceed?" --width 360 --height 300; then + +if [[ "$DISABLE_ZENITY" == "1" ]] || zenity --question --title "Docker" --text "This will install Docker Engine. Proceed?" --width 360 --height 300; then sudo_rq docker_in zeninf "Setup complete. You may install Portainer CE to manage Docker after rebooting." diff --git a/p3/scripts/devs/mise.sh b/p3/scripts/devs/mise.sh index c69f4892..b9356840 100755 --- a/p3/scripts/devs/mise.sh +++ b/p3/scripts/devs/mise.sh @@ -33,4 +33,4 @@ if [ -f $HOME/.config/fish/config.fish ]; then fi zeninf "$msg282" xdg-open https://mise.jdx.dev/walkthrough.html -exit 0 \ No newline at end of file +exit 0