From 36d3c6f99259f5800ed8f51023b0bf30d13dfdf8 Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Sat, 16 May 2026 15:51:10 +0200 Subject: [PATCH] feat(rewrite): Config preparations --- archmgr/__init__.py | 4 +- archmgr/config/{pkgs.py => boot/__init__.py} | 0 archmgr/config/cmds/__init__.py | 0 archmgr/config/filesystem/__init__.py | 0 archmgr/config/filesystem/files.py | 1 + archmgr/config/filesystem/mounts.py | 0 archmgr/config/filesystem/symlinks.py | 0 archmgr/config/git/__init__.py | 2 + archmgr/config/pkgs/__init__.py | 0 archmgr/config/pkgs/pkg_mgr.py | 6 ++ archmgr/config/pkgs/reflector.py | 2 + archmgr/config/pkgs/repos.py | 2 + archmgr/config/templates/__init__.py | 36 +++++++ archmgr/config/themes/__init__.py | 0 archmgr/config/themes/custom.py | 14 +++ archmgr/config/themes/set_theme.py | 2 + archmgr/handlers/pull.py | 0 archmgr/handlers/push.py | 0 archmgr/handlers/show/__init__.py | 0 archmgr/util/diff.py | 34 ++++++ archmgr/util/git.py | 105 +++++++++++++++++++ archmgr/util/input/__init__.py | 44 ++++++++ archmgr/util/input/password_mgr.py | 70 +++++++++++++ archmgr/util/pacman.py | 64 +++++++++++ archmgr/util/printing/__init__.py | 2 + archmgr/util/printing/diff.py | 64 +++++++++++ archmgr/util/printing/list.py | 20 ++++ archmgr/util/regex.py | 0 archmgr/util/render/__init__.py | 0 test.py | 1 + 30 files changed, 471 insertions(+), 2 deletions(-) rename archmgr/config/{pkgs.py => boot/__init__.py} (100%) create mode 100644 archmgr/config/cmds/__init__.py create mode 100644 archmgr/config/filesystem/__init__.py create mode 100644 archmgr/config/filesystem/files.py create mode 100644 archmgr/config/filesystem/mounts.py create mode 100644 archmgr/config/filesystem/symlinks.py create mode 100644 archmgr/config/git/__init__.py create mode 100644 archmgr/config/pkgs/__init__.py create mode 100644 archmgr/config/pkgs/pkg_mgr.py create mode 100644 archmgr/config/pkgs/reflector.py create mode 100644 archmgr/config/pkgs/repos.py create mode 100644 archmgr/config/templates/__init__.py create mode 100644 archmgr/config/themes/__init__.py create mode 100644 archmgr/config/themes/custom.py create mode 100644 archmgr/config/themes/set_theme.py create mode 100644 archmgr/handlers/pull.py create mode 100644 archmgr/handlers/push.py create mode 100644 archmgr/handlers/show/__init__.py create mode 100644 archmgr/util/diff.py create mode 100644 archmgr/util/git.py create mode 100644 archmgr/util/input/__init__.py create mode 100644 archmgr/util/input/password_mgr.py create mode 100644 archmgr/util/pacman.py create mode 100644 archmgr/util/printing/__init__.py create mode 100644 archmgr/util/printing/diff.py create mode 100644 archmgr/util/printing/list.py create mode 100644 archmgr/util/regex.py create mode 100644 archmgr/util/render/__init__.py diff --git a/archmgr/__init__.py b/archmgr/__init__.py index cf8b5c9..7bbf0b9 100755 --- a/archmgr/__init__.py +++ b/archmgr/__init__.py @@ -4,6 +4,6 @@ # TODO: Re-export the config stuff if __name__ == "__main__": - import app as _app + from app import run as _run - _app.run() + _run() diff --git a/archmgr/config/pkgs.py b/archmgr/config/boot/__init__.py similarity index 100% rename from archmgr/config/pkgs.py rename to archmgr/config/boot/__init__.py diff --git a/archmgr/config/cmds/__init__.py b/archmgr/config/cmds/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archmgr/config/filesystem/__init__.py b/archmgr/config/filesystem/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archmgr/config/filesystem/files.py b/archmgr/config/filesystem/files.py new file mode 100644 index 0000000..33c9b8f --- /dev/null +++ b/archmgr/config/filesystem/files.py @@ -0,0 +1 @@ +# Declare a file explicitly if it has different permissions diff --git a/archmgr/config/filesystem/mounts.py b/archmgr/config/filesystem/mounts.py new file mode 100644 index 0000000..e69de29 diff --git a/archmgr/config/filesystem/symlinks.py b/archmgr/config/filesystem/symlinks.py new file mode 100644 index 0000000..e69de29 diff --git a/archmgr/config/git/__init__.py b/archmgr/config/git/__init__.py new file mode 100644 index 0000000..68650cc --- /dev/null +++ b/archmgr/config/git/__init__.py @@ -0,0 +1,2 @@ +def clone_git_repo(repo_url: str, clone_path: str, branch: str = "DEFAULT"): + pass diff --git a/archmgr/config/pkgs/__init__.py b/archmgr/config/pkgs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archmgr/config/pkgs/pkg_mgr.py b/archmgr/config/pkgs/pkg_mgr.py new file mode 100644 index 0000000..3c81797 --- /dev/null +++ b/archmgr/config/pkgs/pkg_mgr.py @@ -0,0 +1,6 @@ +def add_packages(pkgs: list[str]): + pass + + +def add_package_bundles(pkgs: list[str]): + pass diff --git a/archmgr/config/pkgs/reflector.py b/archmgr/config/pkgs/reflector.py new file mode 100644 index 0000000..738dd6a --- /dev/null +++ b/archmgr/config/pkgs/reflector.py @@ -0,0 +1,2 @@ +def enable_reflector(): + pass diff --git a/archmgr/config/pkgs/repos.py b/archmgr/config/pkgs/repos.py new file mode 100644 index 0000000..639e407 --- /dev/null +++ b/archmgr/config/pkgs/repos.py @@ -0,0 +1,2 @@ +def enable_repo(name: str): + pass diff --git a/archmgr/config/templates/__init__.py b/archmgr/config/templates/__init__.py new file mode 100644 index 0000000..f516c57 --- /dev/null +++ b/archmgr/config/templates/__init__.py @@ -0,0 +1,36 @@ +def add_template_data(name: str, data: str): + """Replace all occurrences of variable specified by `name` by + the specified data + + Args: + name: The variable to replace + data: The data to replace it with + """ + pass + + +def add_template_array(name: str, template: str, data: list[str]): + """Replace all occurrences of variable specified by `name` in the files with + the specified template, where {{ data }} is replaced with the each element + of the data argument + + Args: + name: The name of the variable to replace + template: + Template syntax, with {{ data }} as variable, see example below or docs. + Recursive replacement is supported up to one layer deep + data: A list of data that is substituted into the {{ data }} variable in the template + + Example: + ```python + add_template_array("test", "- Hello World {{ data }}\\n", [0, 1]) + ``` + for example replaces {{ test }} in a file with + ``` + - Hello World 0 + + - Hello World 1 + ``` + """ + # TODO: Recursive replacement + pass diff --git a/archmgr/config/themes/__init__.py b/archmgr/config/themes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archmgr/config/themes/custom.py b/archmgr/config/themes/custom.py new file mode 100644 index 0000000..428d5c0 --- /dev/null +++ b/archmgr/config/themes/custom.py @@ -0,0 +1,14 @@ +from typing import Literal + + +def add_custom_theme(): + pass + + +def custom_theme_color_source(src: Literal["wallpaper"] | Literal["default"]): + pass + + +# TODO: For desktop environments, what command?? +def set_wallpaper(path: str): + pass diff --git a/archmgr/config/themes/set_theme.py b/archmgr/config/themes/set_theme.py new file mode 100644 index 0000000..fd081fd --- /dev/null +++ b/archmgr/config/themes/set_theme.py @@ -0,0 +1,2 @@ +def set_gtk_theme(): + pass diff --git a/archmgr/handlers/pull.py b/archmgr/handlers/pull.py new file mode 100644 index 0000000..e69de29 diff --git a/archmgr/handlers/push.py b/archmgr/handlers/push.py new file mode 100644 index 0000000..e69de29 diff --git a/archmgr/handlers/show/__init__.py b/archmgr/handlers/show/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archmgr/util/diff.py b/archmgr/util/diff.py new file mode 100644 index 0000000..c87b5bf --- /dev/null +++ b/archmgr/util/diff.py @@ -0,0 +1,34 @@ +from typing import List + + +def pkg_diff(target: List[str], actual: List[str]) -> tuple[List[str], List[str]]: + """Compute a diff between target packages and installed packages + + Args: + target: The target packages + actual: The actually installed packages + + Returns: + A tuple with first the missing (not installed) packages + and second the extraneous (to be uninstalled) packages + """ + removed = [] + diffed_out = [] + for pkg in actual: + try: + diffed_out.append(target.index(pkg)) + except Exception: + removed.append(pkg) + + diffed_out.reverse() + new_pkgs = [] + if len(diffed_out) > 0: + curr = diffed_out.pop() + for i, pkg in enumerate(target): + if i != curr: + new_pkgs.append(pkg) + else: + if len(diffed_out) > 0: + curr = diffed_out.pop() + + return (new_pkgs, removed) diff --git a/archmgr/util/git.py b/archmgr/util/git.py new file mode 100644 index 0000000..2d1775d --- /dev/null +++ b/archmgr/util/git.py @@ -0,0 +1,105 @@ +import os +import subprocess + + +def init(dirname: str) -> bool: + """Initialize a new git repo in the directory + + Args: + dirname: The directory to create it in + + Returns: + True on success, False on error + """ + return subprocess.call("git init", cwd=dirname) == 0 + + +def commit(msg: str): + """Create a new commit + + Args: + msg: The commit message + + Returns: + True on success, False on error + """ + dir = os.getcwd() + return subprocess.call('git commit -am "' + msg + '"', cwd=dir) == 0 + + +def branch_add(name: str): + """Create a new git branch + + Args: + name: The name of the branch to create + + Returns: + True on success, False on error + """ + dir = os.getcwd() + return subprocess.call("git branch " + name, cwd=dir) == 0 + + +def branch_switch(name: str): + """Switch to a git branch + + Args: + name: The name of the branch to switch to + + Returns: + True on success, False on error + """ + dir = os.getcwd() + return subprocess.call("git checkout " + name, cwd=dir) == 0 + + +def branch_show(): + """Create a new git branch + + Args: + name: The name of the branch to create + + Returns: + True on success, False on error + """ + dir = os.getcwd() + return subprocess.call("git branch", cwd=dir) == 0 + + +def branch_delete(name: str): + """Delete a git branch + + Args: + name: The name of the branch to delete + + Returns: + True on success, False on error + """ + dir = os.getcwd() + return subprocess.call("git branch -d " + name, cwd=dir) == 0 + + +def pull(rebase: bool = False): + """Pull git changes + + Args: + rebase: Whether to rebase or not + + Returns: + True on success, False on error + """ + dir = os.getcwd() + return subprocess.call("git pull" + (" --rebase" if rebase else ""), cwd=dir) == 0 + + +def push(force: bool = False): + """Push git changes + + Args: + force: Whether to force push + + Returns: + True on success, False on error + """ + dir = os.getcwd() + return subprocess.call("git push" + (" --force" if force else ""), cwd=dir) == 0 diff --git a/archmgr/util/input/__init__.py b/archmgr/util/input/__init__.py new file mode 100644 index 0000000..7c36ad2 --- /dev/null +++ b/archmgr/util/input/__init__.py @@ -0,0 +1,44 @@ +from typing import Optional + +from password_mgr import PasswordManager + + +def choice(default: str, options: str, msg: str) -> str: + default = default.lower() + formatted_options = "".join( + [(opt + "/" if opt != default else default.upper() + "/") for opt in options] + )[:-1] + choice = input(msg + " [" + formatted_options + "] ") + if choice == "": + return default + else: + return choice.lower() + + +def confirm(is_true_default: bool, msg: Optional[str] = ""): + return ( + choice( + "y" if is_true_default else "n", + "yn", + (msg if msg != "" and msg != None else "Do you really want to continue?"), + ) + == "y" + ) + + +def confirm_overwrite(msg: Optional[str] = ""): + return confirm( + False, (msg if msg != "" else "Do you really want to overwrite your changes?") + ) + + +# Use class to store the password +pw_manager = PasswordManager() + + +def password(): + return pw_manager.get() + + +def unlock_sudo(): + return pw_manager.validate() diff --git a/archmgr/util/input/password_mgr.py b/archmgr/util/input/password_mgr.py new file mode 100644 index 0000000..f6dcab7 --- /dev/null +++ b/archmgr/util/input/password_mgr.py @@ -0,0 +1,70 @@ +import getpass +import time +import colorama +import subprocess as sp + + +class PasswordManager: + _pw = "" + _wrong_cnt = 0 + _valid = False + + def get(self, msg: str = ""): + """Get the user's password (uses cached password if PW is valid) + Otherwise prompts user + + Args: + msg: The message to use for the password prompt + + Returns: + The user's password + """ + if self._valid or self.validate(): + return self._pw + + if msg != "": + self._pw = getpass.getpass(msg) + else: + self._pw = getpass.getpass() + if self._pw != "": + if not self.validate(): + print( + colorama.Fore.RED + "Error:", + colorama.Style.RESET_ALL + "Invalid Password. Please try again", + ) + return self.get(msg) + return self._pw + else: + time.sleep(1) + print( + colorama.Fore.RED + "Error:", + colorama.Style.RESET_ALL + "Password cannot be empty", + ) + return self.get(msg) + + def validate(self) -> bool: + """Validate that the password is correct by running sudo command + + Returns: + True if password is valid, False otherwise + """ + + def helper(): + if self._pw == "": + return False + try: + sp.run( + ["sudo", "-k"], capture_output=True, text=True + ).check_returncode() + sp.run( + ["sudo", "-S", "echo"], + capture_output=True, + input=self._pw, + text=True, + ).check_returncode() + except Exception: + return False + return True + + self._valid = helper() + return self._valid diff --git a/archmgr/util/pacman.py b/archmgr/util/pacman.py new file mode 100644 index 0000000..9ec4ced --- /dev/null +++ b/archmgr/util/pacman.py @@ -0,0 +1,64 @@ +import subprocess as sp +from typing import List + +from util.input import password + +pkg_manager = ["yay", "--noconfirm"] + + +def list_explicitly_installed() -> List[str]: + """List all packages explicitly installed + + Returns: + The package list + """ + return run_pkg_manager_cmd(["-Qeq"]).stdout.split() + + +def remove_orphans() -> bool: + """Removes all orphan packages + + Returns: + True if successful, False otherwise + """ + return ( + sp.run( + ["pacman", "-Qdtq", "|", "sudo", "pacman", "-Rns", "--noconfirm", "-"], + capture_output=True, + ).returncode + == 0 + ) + + +def uninstall_package_list(pkgs: List[str]) -> bool: + """Uninstall all packages in the list + + Args: + pkgs: A list of packages to uninstall + + Returns: + True if successful, False otherwise + """ + # TODO: Add guard to protect against uninstalling archmgr and confirm uninstalling crucial pkgs + # pkgs.index("archmgr") + # TODO: Consider if we want to print out stdout and stderr on err + return run_pkg_manager_cmd(["-Rs", *pkgs], True).returncode == 0 + + +def install_package_list(pkgs: List[str]) -> bool: + """Install all packages in the list + + Args: + pkgs: A list of packages to install + """ + return run_pkg_manager_cmd(["-S", *pkgs], True).returncode == 0 + + +def run_pkg_manager_cmd( + args: List[str], use_sudo: bool = False +) -> sp.CompletedProcess[str]: + if use_sudo: + pw = password() + return sp.run([*pkg_manager, *args], capture_output=True, text=True, input=pw) + else: + return sp.run([*pkg_manager, *args], capture_output=True, text=True) diff --git a/archmgr/util/printing/__init__.py b/archmgr/util/printing/__init__.py new file mode 100644 index 0000000..d1fbd75 --- /dev/null +++ b/archmgr/util/printing/__init__.py @@ -0,0 +1,2 @@ +import list +import diff diff --git a/archmgr/util/printing/diff.py b/archmgr/util/printing/diff.py new file mode 100644 index 0000000..13633cf --- /dev/null +++ b/archmgr/util/printing/diff.py @@ -0,0 +1,64 @@ +from typing import List +import colorama as cl + +from list import print_list + + +def print_diff(add: List[str], remove: List[str]): + if len(add) == 0 and len(remove) == 0: + print( + cl.Fore.BLUE + + "-->" + + cl.Style.DIM + + cl.Fore.GREEN + + " No packages changes" + + cl.Style.RESET_ALL, + ) + # Packages to be installed + if len(add) == 0: + print( + cl.Fore.BLUE + + "-->" + + cl.Style.DIM + + cl.Fore.GREEN + + " No packages to be installed" + + cl.Style.RESET_ALL, + ) + else: + print( + cl.Fore.GREEN + "==>", + cl.Style.RESET_ALL + "Packages that will be", + cl.Fore.GREEN + "installed" + cl.Style.RESET_ALL, + ) + print_list(add) + print() + + # Packages to be removed + if len(remove) == 0: + print( + cl.Fore.BLUE + + "-->" + + cl.Style.DIM + + cl.Fore.GREEN + + " No packages to be uninstalled" + + cl.Style.RESET_ALL, + ) + else: + print( + cl.Fore.GREEN + "==>", + cl.Style.RESET_ALL + "Packages that will be", + cl.Fore.RED + "uninstalled" + cl.Style.RESET_ALL, + ) + print_list(remove) + print() + + # Ask user to confirm + print( + cl.Fore.GREEN + "==>", + cl.Style.RESET_ALL + + f"Transaction (packages): {cl.Fore.BLUE + str(len(add)) + cl.Style.RESET_ALL}", + cl.Fore.GREEN + "installed", + cl.Style.RESET_ALL + + f"and {cl.Fore.BLUE + str(len(remove)) + cl.Style.RESET_ALL}", + cl.Fore.RED + "uninstalled" + cl.Style.RESET_ALL, + ) diff --git a/archmgr/util/printing/list.py b/archmgr/util/printing/list.py new file mode 100644 index 0000000..350d850 --- /dev/null +++ b/archmgr/util/printing/list.py @@ -0,0 +1,20 @@ +from typing import Any, List +import colorama as cl + + +def count_digits(number: int): + return len(str(number)) + + +def print_list(list: List[Any]): + digit_count = count_digits(len(list)) + for i, pkg in enumerate(list): + print( + " " + + cl.Fore.BLUE + + cl.Style.DIM + + (" " * (digit_count - count_digits(i + 1))) + + str(1 + i) + + cl.Style.RESET_ALL + " ", + pkg, + ) diff --git a/archmgr/util/regex.py b/archmgr/util/regex.py new file mode 100644 index 0000000..e69de29 diff --git a/archmgr/util/render/__init__.py b/archmgr/util/render/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test.py b/test.py index e69de29..681437e 100644 --- a/test.py +++ b/test.py @@ -0,0 +1 @@ +import archmgr