Compare commits

..

20 Commits

Author SHA1 Message Date
janishutz 2fd69cc595 feat(init): Templates handling 2026-04-25 16:43:53 +02:00
janishutz 541a876307 feat(config): Schema validator, initial config merger setup 2026-04-23 11:31:01 +02:00
janishutz 5126e0373f feat(PKGBUILD): First setup 2026-04-23 11:30:31 +02:00
janishutz 299ac49b40 feat(schema): Mostly finish the config options 2026-04-18 14:48:52 +02:00
janishutz b6901b59e6 feat(schema): More described opts 2026-04-17 16:57:07 +02:00
janishutz bcd0339d88 feat: Start config schema 2026-04-17 15:41:07 +02:00
janishutz 4017e0263b chore: Note 2026-04-17 11:33:51 +02:00
janishutz b71d18cdc9 chore: Notes 2026-04-16 16:42:22 +02:00
janishutz 043b2618a3 chore: some improvements towards packaging 2026-04-16 16:02:39 +02:00
janishutz c264a5bea2 feat: Diff packages 2026-04-16 16:02:15 +02:00
janishutz bb123c23a1 refactor(pw): Improve handling of password input 2026-04-16 16:01:56 +02:00
janishutz 414c065df4 refactor(printing): Improve handling of list printing 2026-04-16 16:00:58 +02:00
janishutz 35c976fcac chore(notes): Add some ideas 2026-04-16 16:00:33 +02:00
janishutz b8e2d68469 feat: Improved package diff printing 2026-04-15 17:15:21 +02:00
janishutz 5734c0d524 feat: printing utils 2026-04-15 16:51:55 +02:00
janishutz f5386d0e98 feat: Add setup function 2026-04-15 15:59:36 +02:00
janishutz ead9e3a3f7 fix: no importing, catch errors, ascii art 2026-04-10 08:59:35 +02:00
janishutz 711b89a0d6 feat(pacman): Add needed commands 2026-04-10 08:58:54 +02:00
janishutz 0697bef7d5 feat(input): Password prompt, rename 2026-04-10 08:58:40 +02:00
janishutz 6018d6256d feat(git): all needed subcommands (commit, branches, push, pull) 2026-04-10 08:58:17 +02:00
30 changed files with 1018 additions and 59 deletions
+28
View File
@@ -0,0 +1,28 @@
# Maintainer: Janis Hutz <development@janishutz.com>
pkgname=archmgr-git
pkgver=0.0.0
pkgrel=1
pkgdesc='A nixos-like declarative config and package manager for Arch Linux'
arch=('any')
url="https://github.com/janishutz/archmgr"
license=('GPL3')
depends=('python', 'python-pyaml')
makedepends=('git')
provides=('archmgr')
conflicts=('archmgr')
source=("$pkgname"::git+${url}.git)
sha256sums=('SKIP') # TODO: Add?
pkgver() {
cd "${pkgname}"
# TODO: For the non-git pkgbuild, need to use different output
printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
package() {
cd "${pkgname}"
# TODO: Need to finish
}
Regular → Executable
+17 -5
View File
@@ -1,3 +1,6 @@
#!/usr/bin/env python3
# PYTHON_ARGCOMPLETE_OK
import cli.args as cliargs import cli.args as cliargs
import commands.commit as commit import commands.commit as commit
@@ -5,7 +8,10 @@ import commands.config as config
import commands.init as init import commands.init as init
import commands.pull as pull import commands.pull as pull
import commands.push as push import commands.push as push
import commands.prepare as setup
from config import load_config
if __name__ == "__main__":
args, ap = cliargs.add_cli_args() args, ap = cliargs.add_cli_args()
if args.cmd == None: if args.cmd == None:
@@ -13,8 +19,7 @@ if args.cmd == None:
print("\nSpecify a subcommand, '-h' or '-v'") print("\nSpecify a subcommand, '-h' or '-v'")
exit(64) exit(64)
print( print("""
"""
_ _
( ) ( )
_ _ _ __ ___| |__ ___ ___ __ _ __ _ _ _ __ ___| |__ ___ ___ __ _ __
@@ -23,16 +28,23 @@ print(
\\__ _)_) \\____)_) (_)_) (_) (_)\\__ |_) \\__ _)_) \\____)_) (_)_) (_) (_)\\__ |_)
( )_) | ( )_) |
\\___/ \\___/
""" """)
)
print(load_config("config.yml"))
try:
if args.cmd == "commit": if args.cmd == "commit":
commit.commit(args.dry_run, args.force, args.no_commit) commit.commit(args.force, args.no_render)
elif args.cmd == "config": elif args.cmd == "config":
config.config() config.config()
elif args.cmd == "init": elif args.cmd == "init":
init.init(args.force) init.init(args.force)
elif args.cmd == "setup":
setup.setup()
elif args.cmd == "pull": elif args.cmd == "pull":
pull.pull(args.rebase, args.apply) pull.pull(args.rebase, args.apply)
elif args.cmd == "push": elif args.cmd == "push":
push.push(args.force) push.push(args.force)
except KeyboardInterrupt as e:
exit(130)
except Exception as e:
raise e
+5 -8
View File
@@ -21,18 +21,15 @@ def add_cli_args():
commit = sp.add_parser( commit = sp.add_parser(
"commit", help="apply pending changes and commit to git repo" "commit", help="apply pending changes and commit to git repo"
) )
commit.add_argument("-f", "--force", help="force apply", action="store_true") commit.add_argument("-f", "--force", help="force apply (skips prompts)", action="store_true")
commit.add_argument( commit.add_argument(
"--no-render", "-r", help="do not re-render renderables", action="store_true" "--no-render", "-r", help="do not re-render renderables", action="store_true"
) )
commit.add_argument(
"--dry-run",
"-d",
help="print out files that would be changed",
action="store_true",
)
sp.add_parser("config", help="prints information about your config") # TODO: Allow changing things like config path
sp.add_parser("config", help="prints information about your config and change some of them")
sp.add_parser("setup", help="Do initial setup, like installing required tools")
init = sp.add_parser("init", help="initialize a new archmgr repository") init = sp.add_parser("init", help="initialize a new archmgr repository")
init.add_argument( init.add_argument(
+20 -6
View File
@@ -1,8 +1,22 @@
from commands.util.choice import confirm_overwrite 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
def commit(dry_run: bool = False, force: bool = False): def commit(force: bool = False, no_render: bool = False):
if dry_run: """Commit the changes to the system
pass
print("Commit, force:", force) Args:
print(confirm_overwrite()) 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([], 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)
+2 -4
View File
@@ -1,6 +1,4 @@
def config(): def config():
print( print("""
"""
Your config can be found at Your config can be found at
""" """)
)
+18 -4
View File
@@ -1,14 +1,28 @@
import os import os
import shutil
import commands.util.git as git import commands.util.git as git
from commands.util.input_mgr import confirm
def init(force: bool = False): def init(force: bool = False):
dir = os.getcwd() dir = os.getcwd()
script_dir = os.path.dirname(os.path.realpath(__file__))
if force: if force:
[os.remove(dir) for dir in os.listdir(dir)] 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) git.init(dir)
os.mkdir(dir + "/config") os.mkdir(dir + "/config")
os.mkdir(dir + "/etc") os.mkdir(dir + "/db")
with open(dir + "/config.yaml") as file: os.mkdir(dir + "/system")
file.write("") os.mkdir(dir + "/includes")
shutil.copy(script_dir + "/templates/config/config.yml", dir + "/config.yml")
shutil.copy(script_dir + "/templates/config/system/", dir + "/includes/system/")
shutil.copy(script_dir + "/templates/config/templates/", dir + "/includes/templates/")
shutil.copy(script_dir + "/templates/README.md", dir + "/README.md")
print("Initialized a new archmgr repository") print("Initialized a new archmgr repository")
# 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?)
# TODO: Consider collecting function -> If no files present, will only collect the pkgs, else also the files
# TODO: Config folder instead of single config file
# TODO: Also store the folder name of the config folder in that repo (needs to be easily changeable for user!)
+30
View File
@@ -0,0 +1,30 @@
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
+18 -9
View File
@@ -1,13 +1,22 @@
from typing import List from typing import List
def compute_diff(paths: List[str]): def pkg_diff(target: List[str], actual: List[str]) -> tuple[List[str], List[str]]:
pass """Compute a diff between target packages and installed packages
Args:
def file_diff(path: str): target: The target packages
pass actual: The actually installed packages
Returns:
def pkg_diff(): A tuple with first the missing (not installed) packages
and second the extraneous (to be uninstalled) packages
"""
for i, pkg in enumerate(actual):
try:
idx = target.index(pkg)
target.pop(idx)
actual.pop(i)
except Exception:
pass pass
return (target, actual)
+102 -2
View File
@@ -1,5 +1,105 @@
import os
import subprocess import subprocess
def init(dirname: str): def init(dirname: str) -> bool:
subprocess.call("git init", cwd=dirname) """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
@@ -1,5 +1,7 @@
from typing import Optional from typing import Optional
from commands.util.password_manager import PasswordManager
def choice(default: str, options: str, msg: str) -> str: def choice(default: str, options: str, msg: str) -> str:
default = default.lower() default = default.lower()
@@ -28,3 +30,15 @@ def confirm_overwrite(msg: Optional[str] = ""):
return confirm( return confirm(
False, (msg if msg != "" else "Do you really want to overwrite your changes?") 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
@@ -0,0 +1,64 @@
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
@@ -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
+64
View File
@@ -0,0 +1,64 @@
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
@@ -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,
)
+384
View File
@@ -0,0 +1,384 @@
{
"$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": {
"enabled_repos": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"description": "The repos to set up",
"properties": {
"name": {
"type": "string",
"description": "The repositories to set up",
"pattern": "^(core|extra|core-testing|extra-testing|multilib|multilib-testing|[a-z0-9-]+(?=[a-z0-9]$))"
},
"setup_cmds": {
"type": "array",
"description": "The commands to run to set it up (optional if any of the explicitly supported ones)",
"items": {
"type": "string",
"description": "Command to run"
}
},
"mirrors": {
"type": "object",
"description": "Configure the mirrors to use",
"properties": {
"use_default": {
"type": "boolean",
"description": "Whether to use the default mirrors or not",
"default": true
},
"extra_mirrors": {
"type": "array",
"description": "Any extra mirrors you want to add. At least one mirror needs to be put here if use_default is false. Order matters",
"items": {
"type": "string"
}
}
}
}
}
}
},
"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
},
"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
}
},
"boot": {
"type": "object",
"description": "Settings for the bootloader, such as theme, using os-prober, etc",
"properties": {
"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": {
"type": "object",
"description": "Configuration for the bootloader theme",
"properties": {
"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\\/.-]+))\\/$"
}
},
"additionalProperties": false,
"required": [
"folder"
]
},
"os_prober": {
"type": "boolean",
"description": "Whether to enable OS prober to search for other operating systems"
}
},
"additionalProperties": false,
"required": [
"bootloader",
"esp_dir"
]
},
"themes": {
"type": "object",
"properties": {}
},
"git": {
"type": "object",
"description": "Automatically set up credential manager and clone repos",
"properties": {
"creds": {
"type": "object",
"description": "Which git services to log into",
"properties": {
"manager": {
"type": "string",
"description": "The git credential manager to use. Set to none if you don't want one (default)",
"pattern": "^(git-credential-manager|none)",
"default": "none"
}
},
"additionalProperties": false
},
"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": {
"name": {
"type": "string",
"description": "The name that appears in the template"
},
"data": {
"type": "string",
"description": "The data that is to be inserted"
}
},
"required": [
"name",
"data"
],
"additionalProperties": false
}
},
"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
@@ -0,0 +1,58 @@
# 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:
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)"
+31
View File
@@ -0,0 +1,31 @@
from typing import Any, cast
import yaml
from config import validator
from config.merger import merge_configs
def _load_config_file(file: str):
with open(file, "r") as f:
parsed = yaml.load(f, Loader=yaml.FullLoader)
return parsed
def load_config(file: str):
# Load and validate initial config
try:
loaded_conf = _load_config_file(file)
except Exception:
return {}
if not validator.validate(loaded_conf):
return {}
conf = cast(dict[str, Any], loaded_conf)
requires = cast(list[str], conf["requires"])
conf.pop("requires")
# Recursively load files
for conf_file in requires:
conf = merge_configs(conf, load_config(conf_file))
return conf
+6
View File
@@ -0,0 +1,6 @@
def merge_configs(config: dict, new_config: dict):
if len(new_config) == 0 or len(config) == 0:
return config
# Merge configs
return config
+18
View File
@@ -0,0 +1,18 @@
import json
import jsonschema
with open("config.schema.json") as file:
schema = json.load(file)
def validate(config):
try:
jsonschema.validate(config, schema)
except jsonschema.SchemaError:
print("Schema invalid")
return False
except jsonschema.ValidationError:
print("Config invalid")
return False
return True
+24
View File
@@ -6,3 +6,27 @@
- Copy normal config files into correct directories - Copy normal config files into correct directories
- Render and copy renderables - Render and copy renderables
- Retrieve explicitly installed packages and remove those that are not present in goal and install those that are not present in current state - Retrieve explicitly installed packages and remove those that are not present in goal and install those that are not present in current state
# Ideas
- [ ] function to collect new configs
- [ ] config options for users and groups
- [ ] presets for things like desktops (like Hyprland)
- [ ] config options for the template rendering
- [ ] config options for themes
- [ ] grub config
- [ ] Dynamic selection of more configs (i.e. require syntax)
- [ ] Own config syntax?
- [ ] 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]$))`
+1
View File
@@ -1,3 +1,4 @@
pyyaml
pylette pylette
argcomplete argcomplete
inquirer inquirer
+3
View File
@@ -0,0 +1,3 @@
# 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
View File
View File
View File
View File
View File
View File
View File