feat(rewrite): Start rewrite

This commit is contained in:
2026-05-16 15:00:09 +02:00
parent d391c53c6c
commit 217ccadc74
51 changed files with 22 additions and 1616 deletions
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env python3
# PYTHON_ARGCOMPLETE_OK
# TODO: Re-export the config stuff
if __name__ == "__main__":
import app as _app
_app.run()
Executable → Regular
+5 -8
View File
@@ -1,8 +1,6 @@
#!/usr/bin/env python3 # The main app
# PYTHON_ARGCOMPLETE_OK
from typing import cast from typing import cast
import cli.args as cliargs import cli as cliargs
import commands.commit as commit import commands.commit as commit
import commands.config as config import commands.config as config
@@ -11,10 +9,9 @@ import commands.pull as pull
import commands.push as push import commands.push as push
import commands.prepare as setup import commands.prepare as setup
import commands.show as show import commands.show as show
from config import load_config
from presets import ArchMgrTemplates
if __name__ == "__main__":
def run():
args, ap = cliargs.add_cli_args() args, ap = cliargs.add_cli_args()
if args.cmd == None: if args.cmd == None:
@@ -33,7 +30,7 @@ if __name__ == "__main__":
\\___/ \\___/
""") """)
conf = load_config("config.yml") conf = {}
try: try:
if args.cmd == "commit": if args.cmd == "commit":
commit.commit(conf, args.force, args.no_render) commit.commit(conf, args.force, args.no_render)
+3 -2
View File
@@ -1,7 +1,8 @@
# CLI argument parsing and setup for it
import argparse import argparse
import argcomplete import argcomplete
from cli.commands import commit, config, init, pull, push, show from args import commit, init, pull, push, show
def add_cli_args(): def add_cli_args():
@@ -22,7 +23,7 @@ def add_cli_args():
# └ ┘ # └ ┘
sp.add_parser("setup", help="Do initial setup, like installing required tools") sp.add_parser("setup", help="Do initial setup, like installing required tools")
commit.add_parser(sp) commit.add_parser(sp)
config.add_parser(sp) sp.add_parser("config", help="prints information about your config")
init.add_parser(sp) init.add_parser(sp)
pull.add_parser(sp) pull.add_parser(sp)
push.add_parser(sp) push.add_parser(sp)
-21
View File
@@ -1,21 +0,0 @@
import argparse as ap
def add_parser(sp: ap._SubParsersAction[ap.ArgumentParser]):
config = sp.add_parser(
"config", help="prints information about your config and change some of them"
)
conf_sp = config.add_subparsers(title="config options", dest="conf", required=True)
conf_sp.add_parser("show", help="show config options")
config_update = conf_sp.add_parser(
"update", help="update the configuration from various"
)
conf_update_sp = config_update.add_subparsers(
title="Options for automatic configuration updates",
dest="conf_update",
required=True,
)
conf_update_sp.add_parser(
"pkgs",
help="Update the config to reflect the package state of the current system",
)
-23
View File
@@ -1,23 +0,0 @@
from commands.util import pacman
from commands.util.diff import pkg_diff
from commands.util.input_mgr import confirm, password
from commands.util.printing.diff import print_diff
from config.dtype import ArchMgrConfig
def commit(config: ArchMgrConfig, force: bool = False, no_render: bool = False):
"""Commit the changes to the system
Args:
force: Apply, overriding any changes since the last commit without confirming
no_render: Don't rerender the templates (use cached version).
Will be ignored if cache is unavailable (and prompt the user)
"""
# TODO: Make sure we don't uninstall critical system packages by accident (i.e. prompt user)
# Probably do that check in the pacman util lib tho
add, remove = pkg_diff(config["pkgs"]["individual"] or [], pacman.list_explicitly_installed())
print_diff(add, remove)
if confirm(False, "Do you really want to proceed?"):
pacman.install_package_list(add)
pacman.uninstall_package_list(remove)
-13
View File
@@ -1,13 +0,0 @@
import argparse
from config.dtype import ArchMgrConfig
def config(args: argparse.Namespace, config: ArchMgrConfig):
if args.conf == "show":
print("""
Your config can be found at
""")
print(config)
elif args.conf_update == "pkgs":
print("Updating config's package list")
# TODO: Config saver
-30
View File
@@ -1,30 +0,0 @@
import os
import shutil
import commands.util.git as git
from commands.util.input_mgr import confirm
def init(force: bool = False):
dir = os.getcwd()
script_dir = os.path.dirname(os.path.realpath(__file__))
if force:
if confirm(
False,
"Do you really want to IRREVERSIBLY DELETE the contents of this folder and redo setup?",
):
[os.remove(file) for file in os.listdir(dir)]
git.init(dir)
os.mkdir(dir + "/config")
os.mkdir(dir + "/system")
os.mkdir(dir + "/includes")
shutil.copy(script_dir + "/templates/config.yml", dir + "/config.yml")
shutil.copy(script_dir + "/templates/system/", dir + "/includes/system/")
shutil.copy(script_dir + "/templates/templates/", dir + "/includes/templates/")
shutil.copy(script_dir + "/templates/README.md", dir + "/README.md")
print("Initialized a new archmgr repository")
# TODO: Instead of copying things over, clone the template repo, then copy its contents to the new repo?
# TODO: For the files, store the permissions in a db
# TODO: Warn user to not delete .config/archmgr repo
# TODO: Set up that repo (where to put it? /usr/share? or .config?)
# TODO: Consider collecting function -> If no files present, will only collect the pkgs, else also the files
# TODO: Also store the folder name of the config folder in that repo (needs to be easily changeable for user!)
-30
View File
@@ -1,30 +0,0 @@
import subprocess
from commands.util import pacman
from commands.util.input_mgr import confirm
def setup():
print("==> Installing required packages")
if not pacman.install_package_list(["git"]):
print("Git installation failed")
return
subprocess.run(
["git", "clone", "https://aur.archlinux.org/yay.git"],
cwd="/tmp",
capture_output=True,
)
yay_install = subprocess.run(
["makepkg", "-si"], cwd="/tmp/yay", capture_output=True
)
if yay_install.returncode != 0:
print("==> Installation of yay failed")
if confirm(True, "Do you wish to view the logs?"):
print(yay_install.stdout, "\n", yay_install.stderr)
return
print("==> Installation completed")
# TODO: Check if yay is available before installing
-5
View File
@@ -1,5 +0,0 @@
from config.dtype import ArchMgrConfig
def pull(config: ArchMgrConfig, rebase: bool = False, apply: bool = False):
print("pull")
-2
View File
@@ -1,2 +0,0 @@
def push(force: bool = False):
print("push")
-21
View File
@@ -1,21 +0,0 @@
import argparse
from config.dtype import ArchMgrConfig
from templates import ArchMgrTemplates
# TODO: Templates data
def show(
config: ArchMgrConfig,
spec: ArchMgrTemplates,
args: argparse.Namespace,
):
"""Show details / information about things like contents pkg bundles
Args:
config: The configuration
spec: The archmgr templates that were loaded
kind: The type of information to get
arg: The arg for the command (e.g. bundle name)
"""
print(args)
-34
View File
@@ -1,34 +0,0 @@
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)
-105
View File
@@ -1,105 +0,0 @@
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
-44
View File
@@ -1,44 +0,0 @@
from typing import Optional
from commands.util.password_manager 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()
-64
View File
@@ -1,64 +0,0 @@
import subprocess as sp
from typing import List
from commands.util.input_mgr 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)
-70
View File
@@ -1,70 +0,0 @@
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
-64
View File
@@ -1,64 +0,0 @@
from typing import List
import colorama as cl
from commands.util.printing.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,
)
-20
View File
@@ -1,20 +0,0 @@
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,
)
-58
View File
@@ -1,58 +0,0 @@
# yaml-language-server: $schema=config.schema.json
# TODO: Change the above to an import URL instead
requires:
- path/to/other/configs/relative/to/this # Reads the other configs after finishing this one
pkgs:
individual:
- pkg_name
bundles:
- name: hyprland
repos:
reflector:
enabled: false
countries:
- Switzerland
users:
- username: username
groups:
- group_1
home_dir: True
boot:
managed: True
bootloader: grub
esp_dir: /boot/
theme_folder: ~/.path/to/theme/
os_prober: False
# Also copies over the /etc/default/grub config or equivalent for other supported bootloaders
# TODO: Desktops, login managers, full disk encryption etc configuration?
themes:
gtk: theme_name
qt: theme_name # or use_gtk to use the gtk theme instaed
font: Comfortaa 11 # the font name to be used (also needs to be installed)
icon_theme: candy-icons # The icon theme to use (also needs to be installed)
cursor_theme: oreo_spark_blue_cursors # TODO: Consider if GTK settings file should just be copied
git:
creds:
manager: git-credential-manager # or none
repos:
- url: https://github.com/janishutz/janishutz
clone_path: ~/projects/ # Project location will be clone_path/<repo name>
- url: git@git.janishutz.com:janishutz/nvim
clone_path: ~/projects/ # Project location will be clone_path/<repo name>
template_data:
- name: template_data_name
data: the_data
cmds:
always:
- cmd: "cmd to run every time archmgr is run"
once:
- name: "cmd1"
cmd: "cmd to run on first execution of archmgr (or if not executed previously)"
-460
View File
@@ -1,460 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"type": "object",
"properties": {
"requires": {
"type": "array",
"items": {
"type": "string",
"pattern": "^([a-zA-Z0-9.-_]+\\/)+",
"description": "Path to other configs, relative to this file (e.g. config/pkgs.yaml will expand to dirname(this_file)/config/pkgs.yaml)"
},
"description": "Imports for other config files that will be merged into this one. Precedence order is bottom up (i.e. lowest has highest precedence)"
},
"pkgs": {
"type": "object",
"description": "The packages to be installed",
"properties": {
"individual": {
"type:": "array",
"description": "the packages to be installed, by their package name",
"items": {
"type": "string",
"pattern": "^[a-z0-9-._]+(?=[a-z0-9]$)"
}
},
"repos": {
"type": "object",
"properties": {
"reflector": {
"type": "object",
"description": "Use reflector to update the mirrors",
"properties": {
"enabled": {
"type": "boolean",
"default": false
},
"interval": {
"type": "number",
"description": "The number of days to elapse between reflector reruns"
},
"countries": {
"type": "array",
"description": "The countries in which the should be located (only applies for the main arch repos)",
"items": {
"type": "string",
"pattern": "^[A-Z][a-z]*[a-z]$"
},
"maxItems": 5,
"minItems": 1
},
"count": {
"type": "number",
"description": "The number of mirrors to add to the list",
"maximum": 20,
"minimum": 3
}
},
"required": [
"enabled"
],
"additionalProperties": false
},
"additionalProperties": false
}
},
"bundles": {
"type": "array",
"description": "Bundled packages, installing all the recommended extra software for them (such as hyprland and nvim)",
"maxItems": 1,
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The bundle name",
"pattern": "^[a-z0-9-._]+(?=[a-z0-9]$)"
},
"ignored_pkgs": {
"type": "array",
"description": "List of packages from the bundle that should not be installed",
"items": {
"type": "string",
"pattern": "^[a-z0-9-._]+(?=[a-z0-9]$)"
}
}
},
"additionalProperties": false,
"required": [
"name"
]
}
}
},
"additionalProperties": false
},
"boot": {
"type": "object",
"description": "Settings for the bootloader, such as theme, using os-prober, etc",
"properties": {
"managed": {
"type": "boolean",
"description": "Whether or not to manage the bootloader using archmgr"
},
"bootloader": {
"type": "string",
"description": "The bootloader to use (more coming eventually)",
"pattern": "^(grub)"
},
"esp_dir": {
"type": "string",
"description": "The directory for the bootloader files. Has to end in slash",
"pattern": "^(^(~|\\.|\\.\\.)?\\/)?([\\w\\/.-]+(?!.*[^\\w\\/.-]+))\\/$"
},
"theme_folder": {
"type": "string",
"description": "Where the folder for the theme is found. Can be relative to the config repo or file system and has to end in slash",
"pattern": "^(^(~|\\.|\\.\\.)?\\/)?([\\w\\/.-]+(?!.*[^\\w\\/.-]+))\\/$"
},
"os_prober": {
"type": "boolean",
"description": "Whether to enable OS prober to search for other operating systems"
},
"kernel_params": {
"type": "string",
"description": "The kernel arguments to pass in"
}
},
"additionalProperties": false,
"required": [
"managed"
]
},
"themes": {
"type": "object",
"properties": {
"gtk": {
"type": "string",
"description": "The GTK theme to use. To use the custom generated theme, set this to Adaptive-Theme and set the Qt theme to gtk3"
},
"qt": {
"type": "string",
"description": "The Qt theme to use. Write gtk3 to use the GTK theme instead",
"default": "gtk3"
},
"font": {
"type": "string",
"description": "The font (and size) to use for the interface. Needs to be installed to work",
"default": "Comfortaa 11"
},
"icon_theme": {
"type": "string",
"description": "Which icon theme to use. Needs to be installed to work"
},
"cursor_theme": {
"type": "string",
"description": "The cursor to use. Needs to be installed to work"
},
"selected_custom_theme_color": {
"type": "string",
"description": "The custom theme color to use. archmgr comes with 5 defaults than can be used without custom_theme_colors defined",
"pattern": "^(nordic|deep-dark|material|light|bright|.*)"
},
"color_source": {
"type": "string",
"description": "Where to get the colours from",
"pattern": "^(wallpaper|default)",
"default": "wallpaper"
},
"wallpaper": {
"type": "string",
"description": "Path to the wallpaper to use. If supported by manager for wallpaper, can also be folder. Please note that in that case the theme colours will fall back to the default values and will not use the wallpaper"
},
"custom_theme_colors": {
"type": "array",
"description": "Templates for a custom theme.",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the color theme. This can be used as the"
},
"base": {
"type": "string",
"description": "The name of the custom_theme to base it off of. Defaults to deep-dark, which is a default theme provided by archmgr",
"default": "deep-dark"
},
"variables": {
"type": "object",
"description": "The definition of the colors and other variables that override any set in template_data if present there",
"properties": {
"color-fore-primary": {
"type": "string"
},
"color-fore-accent-1": {
"type": "string"
},
"color-fore-accent-2": {
"type": "string"
},
"color-fore-accent-3": {
"type": "string"
},
"color-back-primary": {
"type": "string"
},
"color-back-accent-1": {
"type": "string"
},
"color-back-accent-2": {
"type": "string"
},
"color-shadow": {
"type": "string"
},
"color-fore-inactive": {
"type": "string"
},
"color-back-inactive": {
"type": "string"
},
"font-primary": {
"type": "string"
},
"font-accent": {
"type": "string"
},
"font-mono": {
"type": "string"
}
},
"additionalProperties": true
}
}
}
}
}
},
"git": {
"type": "object",
"description": "Automatically set up credential manager and clone repos",
"properties": {
"repos": {
"type": "array",
"description": "Which repos to clone (removing one from here doesn't delete it from the system and only pulls if folder does not exist)",
"items": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "git clone URL (ssh or HTTP(S))",
"pattern": "^((https?):\\/\\/(([a-z0-9-]+)((?=\\.))\\.)+[a-z]+(?=\\/)\\/([\\w\\-?.=]+(?=\\/[\\w\\-?.=])\\/)*([\\w\\-?&.=\\/]+(?=[\\w\\-.=\\/]$)))|([a-zA-Z0-9\\-._]+(?=[a-zA-Z0-9])[a-zA-Z0-9])@(([a-z0-9\\-]+(?=\\.))\\.)+[a-z]+(?=:):([a-zA-Z0-9\\-._]+(?=\\/)\\/)+([a-zA-Z0-9\\-._]+(?=[a-zA-Z0-9]$))"
},
"clone_path": {
"type": "string",
"description": "The location on the local system where to put it. Must be absolute path. Parent folders will be created if they don't exist",
"pattern": "^(^(~|\\.|\\.\\.)?\\/)?([\\w\\/.-]+(?!.*[^\\w\\/.-]+))\\/$"
}
},
"additionalProperties": false,
"required": [
"url",
"clone_path"
]
}
},
"additionalProperties": false
}
},
"template_data": {
"type": "array",
"description": "The data to be inserted into the templates",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"string",
"array"
],
"description": "The type of data included. Can be omitted, which then defaults to string",
"default": "string"
}
},
"if": {
"properties": {
"type": {
"const": "array"
}
},
"required": [
"type"
]
},
"then": {
"properties": {
"type": {
"type": "string",
"enum": [
"string",
"array"
],
"description": "The type of data included. Can be omitted, which then defaults to string",
"default": "string"
},
"name": {
"type": "string",
"description": "The name that appears in the template"
},
"items": {
"type": "array",
"items": {
"type": "string"
}
},
"template": {
"type": "string"
}
},
"required": [
"items",
"template",
"name"
],
"additionalProperties": false
},
"else": {
"properties": {
"name": {
"type": "string",
"description": "The name that appears in the template"
},
"type": {
"type": "string",
"enum": [
"string",
"array"
],
"description": "The type of data included. Can be omitted, which then defaults to string",
"default": "string"
},
"data": {
"type": "string",
"description": "The data that is to be inserted"
}
},
"required": [
"data"
],
"additionalProperties": false
},
"required": [
"name"
]
}
},
"symlinks": {
"type": "array",
"description": "Symlinks to create",
"items": {
"type": "object",
"properties": {
"destination": {
"type": "string",
"description": "The directory the link should point to",
"pattern": "^(^(~|\\.|\\.\\.)?\\/)?([\\w\\/.-]+(?!.*[^\\w\\/.-]+))"
},
"location": {
"type": "string",
"description": "What to call the link",
"pattern": "^(^(~|\\.|\\.\\.)?\\/)?([\\w\\/.-]+(?!.*[^\\w\\/.-]+))"
}
}
}
},
"cmds": {
"type": "object",
"properties": {
"once": {
"type": "array",
"description": "Commands to run on only once (uses the name property to determine if it needs to run)",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "This name is used to track if a command was run before."
},
"cmd": {
"type": "string"
},
"hook": {
"type": "string",
"description": "Where in the execution of archmgr to run",
"default": "end",
"pattern": "^(pre-pkg|post-pkg|pre-git|post-git|end)"
},
"user": {
"type": "string",
"default": "root",
"pattern": "^[a-zA-Z0-9\\-._]{2,19}(?=[a-zA-Z0-9]$)",
"description": "The user to run as. Be aware that only the current user's password is available, unless capture_output is set to false"
},
"capture_output": {
"type": "boolean",
"default": true,
"description": "Whether or not to hide the output from the user"
}
},
"additionalProperties": false,
"required": [
"name",
"cmd"
]
}
},
"always": {
"type": "array",
"description": "Commands to run on each apply",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Used to indicate to user what command is executing. If omitted, will be truncated cmd"
},
"cmd": {
"type": "string"
},
"hook": {
"type": "string",
"description": "Where in the execution of archmgr to run",
"default": "end",
"pattern": "^(pre-pkg|post-pkg|pre-git|post-git|end)"
},
"user": {
"type": "string",
"default": "root",
"pattern": "^[a-zA-Z0-9\\-._]{2,19}(?=[a-zA-Z0-9]$)",
"description": "The user to run as. Be aware that only the current user's password is available, unless capture_output is set to false"
},
"capture_output": {
"type": "boolean",
"default": true,
"description": "Whether or not to hide the output from the user"
}
}
}
}
},
"additionalProperties": false
}
},
"required": [
"pkgs",
"boot"
],
"additionalProperties": false
}
-58
View File
@@ -1,58 +0,0 @@
# yaml-language-server: $schema=config.schema.json
# TODO: Change the above to an import URL instead
requires:
- ./config-include-test.yml # Reads the other configs after finishing this one
# - path/to/other/configs/relative/to/this # Reads the other configs after finishing this one
pkgs:
individual:
- acpi
- tk
- tldr
bundles:
- name: hyprland
repos:
reflector:
enabled: false
countries:
- Switzerland
boot:
managed: True
bootloader: grub
esp_dir: /boot/
theme_folder: ~/.path/to/theme/ # This could then be used with git to clone it in
os_prober: False
# Also copies over the /etc/default/grub config or equivalent for other supported bootloaders
# TODO: Desktops, login managers, full disk encryption etc configuration?
themes:
gtk: theme_name
qt: theme_name # or use_gtk to use the gtk theme instaed
font: Comfortaa 11 # the font name to be used (also needs to be installed)
icon_theme: candy-icons # The icon theme to use (also needs to be installed)
cursor_theme: oreo_spark_blue_cursors # TODO: Consider if GTK settings file should just be copied
# TODO: Cursor theme needs more flexibility
git:
repos:
- url: https://github.com/janishutz/janishutz
clone_path: ~/projects/ # Project location will be clone_path/<repo name>
- url: git@git.janishutz.com:janishutz/nvim
clone_path: ~/projects/ # Project location will be clone_path/<repo name>
template_data:
- name: template_data_name
type: array
template: |
test {{ data }}
items:
- test
cmds:
always:
- cmd: "cmd to run every time archmgr is run"
once:
- name: "cmd1"
cmd: "cmd to run on first execution of archmgr (or if not executed previously)"
-108
View File
@@ -1,108 +0,0 @@
from typing import Any, cast
import yaml
from config import validator
from config.dtype import ArchMgrConfig
from config.merger import merge_configs
def load_config_file(file: str):
"""Load and parse the config file.
No verification is done and is not cast
Args:
file: The path to the file to be loaded
Returns:
The loaded and parsed file.
"""
with open(file, "r") as f:
parsed = yaml.load(f, Loader=yaml.FullLoader)
return parsed
def default_config() -> ArchMgrConfig:
"""Get the default configuration
Returns:
The default config
"""
return {
"pkgs": {
"individual": [],
"bundles": [],
"repos": {
"enabled_repos": [
{
"name": "core",
"setup_cmds": [],
"mirrors": {"use_default": True, "extra_mirrors": []},
}
],
"reflector": {
"enabled": False,
"count": 0,
"countries": [],
"interval": 1,
},
},
},
"boot": {
"managed": False,
"bootloader": "grub",
"esp_dir": "/boot",
"os_prober": False,
"theme_folder": "/usr/share/themes/grub",
},
"cmds": {"always": [], "once": []},
"git": {
"repos": [],
"creds": {"manager": "git-credential-manager"},
},
"users": [],
"symlinks": [],
"template_data": [],
"themes": {
"cursor_theme": "oreo_spark_blue_cursor",
"font": "Comfortaa 11",
"gtk": "Adaptive-Theme",
"qt": "gtk3",
"icon_theme": "candy-icons",
},
}
requires_list: list[str] = []
source_list = {} # for each setting, in which files it appears
def load_config(file: str) -> ArchMgrConfig:
"""Load the configuration from the specified file path
Args:
file: Path to the file to be loaded
Returns:
The loaded, validated and parsed config
"""
global requires_list, source_list
# Load and validate initial config
try:
loaded_conf = load_config_file(file)
except Exception:
return default_config()
if not validator.validate(loaded_conf):
return default_config()
configuration = cast(dict[str, Any], loaded_conf)
requires = cast(list[str], configuration.pop("requires"))
# Keep track of all files
requires_list += requires
conf = cast(ArchMgrConfig, configuration)
# Recursively load files
for conf_file in requires:
conf = merge_configs(conf, load_config(conf_file))
return conf
-23
View File
@@ -1,23 +0,0 @@
from typing import Optional, TypedDict
from config.dtype.cmds import ArchMgrCmdsConfig
from config.dtype.git import ArchMgrGitConfig
from config.dtype.others import (
ArchMgrBootConfig,
ArchMgrSymlinkConfig,
ArchMgrTemplateData,
ArchMgrThemeConfig,
ArchMgrUserConfig,
)
from config.dtype.pkgs import ArchMgrPkgConfig
class ArchMgrConfig(TypedDict):
pkgs: ArchMgrPkgConfig
users: Optional[list[ArchMgrUserConfig]]
boot: ArchMgrBootConfig
themes: Optional[ArchMgrThemeConfig]
git: Optional[ArchMgrGitConfig]
template_data: Optional[list[ArchMgrTemplateData]]
symlinks: Optional[list[ArchMgrSymlinkConfig]]
cmds: Optional[ArchMgrCmdsConfig]
-14
View File
@@ -1,14 +0,0 @@
from typing import Optional, TypedDict
class ArchMgrCmdsConfig(TypedDict):
always: Optional[list[ArchMgrCommand]]
once: Optional[list[ArchMgrCommand]]
class ArchMgrCommand(TypedDict):
cmd: str
name: Optional[str]
capture_output: Optional[bool]
hook: Optional[str]
user: Optional[str]
-15
View File
@@ -1,15 +0,0 @@
from typing import Optional, TypedDict
class ArchMgrGitConfig(TypedDict):
creds: Optional[ArchMgrGitCredsConfig]
repos: Optional[list[ArchMgrGitRepoConfig]]
class ArchMgrGitCredsConfig(TypedDict):
manager: Optional[str]
class ArchMgrGitRepoConfig(TypedDict):
clone_path: str
url: str
-34
View File
@@ -1,34 +0,0 @@
from typing import Optional, TypedDict
class ArchMgrUserConfig(TypedDict):
username: str
groups: Optional[list[str]]
home_dir: Optional[bool]
sudo_user: Optional[bool]
class ArchMgrBootConfig(TypedDict):
managed: bool
bootloader: Optional[str]
esp_dir: Optional[str]
theme_folder: Optional[str]
os_prober: Optional[bool]
class ArchMgrThemeConfig(TypedDict):
gtk: Optional[str]
qt: Optional[str]
font: Optional[str]
icon_theme: Optional[str]
cursor_theme: Optional[str]
class ArchMgrTemplateData(TypedDict):
name: str
data: str
class ArchMgrSymlinkConfig(TypedDict):
destination: str
location: str
-35
View File
@@ -1,35 +0,0 @@
from typing import Optional, TypedDict
class ArchMgrPkgConfig(TypedDict):
individual: Optional[list[str]]
repos: Optional[ArchMgrReposConfig]
bundles: Optional[list[ArchMgrBundleConfig]]
class ArchMgrReposConfig(TypedDict):
enabled_repos: Optional[list[ArchMgrRepoSettings]]
reflector: Optional[ArchMgrReflectorConfig]
class ArchMgrReflectorConfig(TypedDict):
enabled: bool
interval: Optional[int]
countries: Optional[list[str]]
count: Optional[int]
class ArchMgrBundleConfig(TypedDict):
name: str
ignored_pkgs: Optional[list[str]]
class ArchMgrRepoSettings(TypedDict):
name: str
setup_cmds: Optional[list[str]]
mirrors: ArchMgrRepoMirrors
class ArchMgrRepoMirrors(TypedDict):
use_default: bool
extra_mirrors: Optional[list[str]]
-45
View File
@@ -1,45 +0,0 @@
from typing import cast
from config.dtype import ArchMgrConfig
def merge_configs(config: ArchMgrConfig, new_config: ArchMgrConfig) -> ArchMgrConfig:
"""Merge two configs, with the new_config taking precedence over the config
in the conflicting fields with arrays and dicts merged
Args:
config: Base config
new_config: Config to merge into the base config
Returns:
The merged config
"""
if len(new_config) == 0 or len(config) == 0:
return config
def combine(a: dict, b: dict):
combined = {}
for key in b:
val = b[key]
try:
a[key]
if isinstance(val, dict):
combined[key] = combine(val, a[key])
elif isinstance(val, list):
combined[key] = val
for v in a[key]:
combined[key].append(v)
else:
combined[key] = val
except KeyError:
combined[key] = val
for key in a:
try:
b[key]
except KeyError:
combined[key] = a[key]
return combined
# Merge configs (using nasty casts)
return cast(ArchMgrConfig, combine(cast(dict, config), cast(dict, new_config)))
-26
View File
@@ -1,26 +0,0 @@
import json
import jsonschema
with open("config.schema.json") as file:
schema = json.load(file)
def validate(config: dict | list):
"""Validate the specified config
Args:
config: The raw configuration
Returns:
True if the config is valid, False otherwise
"""
try:
jsonschema.validate(config, schema)
except jsonschema.SchemaError:
print("Schema invalid")
return False
except jsonschema.ValidationError:
print("Config invalid")
return False
return True
-1
View File
@@ -1 +0,0 @@
# Import the configuration preset templates
-1
View File
@@ -1 +0,0 @@
# TODO: Material colors creator
-2
View File
@@ -1,2 +0,0 @@
def update_users_and_groups():
pass
+5 -82
View File
@@ -1,84 +1,7 @@
# Concept for tracking changes # Concepts
- Create new commit (if possible) ## Config
- If force is unset: Diff system's files against current state by copying them into the current state branch In python, using functions and args for them
- Then, diff the package state against the state branch by dumping it
- Else, or if no diff, continue
- Render and copy config files into correct directories
- Create config files from presets
- Retrieve explicitly installed packages and remove those that are not present in goal and install those that are not present in current state
# Concept for updating config automatically ## Init
1. Track which files contain the setting - Copy
2. If there are multiple and is no array, then simply overwrite the last
3. If there are multiple and is array, then remove from the file where it is present and add to last file in requires list
-> To enable this, update the loader to keep more metadata (list of all requires, list of files for each setting)
# Ideas
- [X] function to collect new configs -> Not smart because templates
- [ ] Templates foreach (array of arrays or something like it)
- [ ] Improved base GTK theme
- [ ] config options for users and groups -> Omitted for now
- [ ] Wallpaper settings for other WM than hyprland
- [ ] presets for things like desktops (like Hyprland)
- [ ] config options for the template rendering
- [ ] config options for themes
- [ ] grub config
- [X] Dynamic selection of more configs (i.e. require syntax)
- [ ] Conditional require
- [X] Own config syntax? -> just use yaml
- [ ] Autocompletion
- [ ] Basic arch install how? -> Probably manual (or semi-automatic)
- [ ] Mounts?
# REGEX
TODO: Improve the below (especially file can be shortened with positive lookahead)
- File: `^(^(~|\\.|\\.\\.)?\\/)?([\\w\\/.-]+(?!.*[^\\w\\/.-]+))`
- Folder: `^(^(~|\\.|\\.\\.)?\\/)?([\\w\\/.-]+(?!.*[^\\w\\/.-]+))\\/$`
- URL (just domain): `^(https?):\\/\\/(([a-z0-9-]+)((?=\\.))\\.)+[a-z]+(?=[a-z]$)`
- Full URL: `^(https?):\\/\\/(([a-z0-9-]+)((?=\\.))\\.)+[a-z]+(?=\\/)\\/([\\w\\-?.=]+(?=\\/[\\w\\-?.=])\\/)*([\\w\\-?&.=\\/]+(?=[\\w\\-.=\\/]$))`
- UNIX username and groups: `^[a-zA-Z0-9\\-._]{2,19}(?=[a-zA-Z0-9]$)`
- Git SSH: `([a-zA-Z0-9\\-._]+(?=[a-zA-Z0-9])[a-zA-Z0-9])@(([a-z0-9\\-]+(?=\\.))\\.)+[a-z]+(?=:):([a-zA-Z0-9\\-._]+(?=\\/)\\/)+([a-zA-Z0-9\\-._]+(?=[a-zA-Z0-9]$))`
## User config schema
This may or may not ever be a thing
```json
"users": {
"type": "array",
"description": "Users to add, including groups. Users will be diffed and removed if they are removed from here. No files are deleted",
"items": {
"type": "object",
"properties": {
"username": {
"type": "string",
"pattern": "^[a-zA-Z0-9\\-._]{2,19}(?=[a-zA-Z0-9]$)"
},
"groups": {
"type": "array",
"description": "The groups to add the user to. Groups are created if they don't exist. User's own group doesn't have to be listed explicitly",
"items": {
"type": "string",
"pattern": "^[a-zA-Z0-9\\-._]{2,19}(?=[a-zA-Z0-9]$)"
}
},
"sudo_user": {
"type": "boolean",
"default": false,
"description": "Whether a user can use sudo or not. Same as appending them to the `wheel` group"
},
"home_dir": {
"type": "boolean",
"description": "Whether to create a home directory for the user or not",
"default": false
}
},
"required": [
"username"
],
"additionalProperties": false
}
},
```
-19
View File
@@ -1,19 +0,0 @@
from config.dtype.others import ArchMgrTemplateData
class ArchMgrTemplates:
pkg_bundles: dict[str, list[str]]
bootloader_config: list[BootLoaderSettings]
theme_templates: dict[str, list[ArchMgrTemplateData]]
class BootLoaderSettings:
# Used to render the included template files
templates: list[ArchMgrTemplateData]
theme_folder: str
conf_build_cmd: str
name: str
def load_templates():
pass
-7
View File
@@ -1,7 +0,0 @@
grub:
name: grub
templates:
- name: "GFX_MODE"
data: "COMP:SCREENWIDTHxCOMP:SCREENHEIGHT"
theme_folder: "ESP_DIR/grub/themes/"
conf_build_cmd: "grub-mkconfig -o ESP_DIR/grub/grub.cfg"
-55
View File
@@ -1,55 +0,0 @@
hyprland:
- hyprland
- hyprlock
- hypridle
- hyprshutdown
- grimblast
- grim
- xdg-desktop-portal-hyprland
- hyprpolkitagent
hyrpland-extra:
- hyprpaper
- wl-clipboard
- cliphist
- hyprlauncher
- hyprpwcenter
- hyprtoolkit
pipewire:
- pipewire
- pipewire-alsa
- pipewire-pulse
- pipewire-jack
- wireplumber
neovim:
- neovim
- lua
- lua-language-server
- tree-sitter
- tree-sitter-cli
- stylua
archives:
- zip
- unzip
bluetooth:
- blueman
- bluez
- bluez-utils
utils:
- fastfetch
- tldr
- gdu
- dig
- glances
- bashtop
# TODO: For all, make it possible to be pkgs plus configs?
yazi:
- yazi
- xdg-desktop-portal-termfilechooser-hunkyburrito-git
- ouch
-2
View File
@@ -1,2 +0,0 @@
nordic:
deep-dark:
-9
View File
@@ -1,9 +0,0 @@
# archmgr data folder
archmgr is a nixos-inspired package and config manager for Arch Linux.
To function, it needs both a configuration file (or multiple) and templates for its provided, preset configurations.
The latter reside in this folder and are a stripped-down version of the default config file.
## Template config folder
This folder contains the template configs for archmgr to copy into the user's newly created config repo
using the init function
View File
View File
-6
View File
@@ -1,6 +0,0 @@
# Templates folder
By convention, put here any yaml files that contain template information.
Ideally, each file in this folder contains config options for one of your programs.
Included you can find an example file for hyprland
View File