Compare commits

...

8 Commits

Author SHA1 Message Date
janishutz 36d3c6f992 feat(rewrite): Config preparations 2026-05-16 15:51:10 +02:00
janishutz 217ccadc74 feat(rewrite): Start rewrite 2026-05-16 15:00:09 +02:00
janishutz d391c53c6c chore: notes 2026-05-16 14:28:24 +02:00
janishutz 24c52c1bba feat(schema): Add array templates 2026-05-16 12:01:40 +02:00
janishutz 80d7b3d86e feat(config): Clean up, add themes options 2026-05-16 11:41:00 +02:00
janishutz 72b477381f chore: more ideas 2026-05-15 17:00:41 +02:00
janishutz e8d8429bc9 feat(config): Prepare for more metadata 2026-05-15 16:58:19 +02:00
janishutz afe8d29340 feat(cli): More argument handling 2026-05-15 16:58:00 +02:00
65 changed files with 100 additions and 1053 deletions
+6
View File
@@ -3,3 +3,9 @@
A nixos-like declarative config and package manager for Arch Linux (or any other distro, with some tweaks). A nixos-like declarative config and package manager for Arch Linux (or any other distro, with some tweaks).
See the [Wiki](https://git.janishutz.com/janishutz/archmgr/wiki) See the [Wiki](https://git.janishutz.com/janishutz/archmgr/wiki)
## WIP
This project is very much Work In Progress.
The configs will likely move to python-based configs from the current yaml-based ones to provide you with more flexibility.
`archmgr` will automatically create a basic setup if you run `archmgr init`, so you can get up and running quickly
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env python3
# PYTHON_ARGCOMPLETE_OK
# TODO: Re-export the config stuff
if __name__ == "__main__":
from app import run as _run
_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 templates 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)
@@ -8,5 +8,6 @@ def add_parser(sp: ap._SubParsersAction[ap.ArgumentParser]):
dest="show", dest="show",
required=True, required=True,
) )
show_sp.add_parser("config", help="show details about your configuration. Alias of config show")
pkgs = show_sp.add_parser("pkgs", help="show details on package presets") pkgs = show_sp.add_parser("pkgs", help="show details on package presets")
pkgs.add_argument("show_pkg") pkgs.add_argument("show_pkg")
+1
View File
@@ -0,0 +1 @@
# Declare a file explicitly if it has different permissions
+2
View File
@@ -0,0 +1,2 @@
def clone_git_repo(repo_url: str, clone_path: str, branch: str = "DEFAULT"):
pass
+6
View File
@@ -0,0 +1,6 @@
def add_packages(pkgs: list[str]):
pass
def add_package_bundles(pkgs: list[str]):
pass
+2
View File
@@ -0,0 +1,2 @@
def enable_reflector():
pass
+2
View File
@@ -0,0 +1,2 @@
def enable_repo(name: str):
pass
+36
View File
@@ -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
+14
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
def set_gtk_theme():
pass
View File
View File
View File
View File
@@ -1,6 +1,6 @@
from typing import Optional from typing import Optional
from commands.util.password_manager import PasswordManager from password_mgr import PasswordManager
def choice(default: str, options: str, msg: str) -> str: def choice(default: str, options: str, msg: str) -> str:
@@ -1,7 +1,7 @@
import subprocess as sp import subprocess as sp
from typing import List from typing import List
from commands.util.input_mgr import password from util.input import password
pkg_manager = ["yay", "--noconfirm"] pkg_manager = ["yay", "--noconfirm"]
+2
View File
@@ -0,0 +1,2 @@
import list
import diff
@@ -1,7 +1,7 @@
from typing import List from typing import List
import colorama as cl import colorama as cl
from commands.util.printing.list import print_list from list import print_list
def print_diff(add: List[str], remove: List[str]): def print_diff(add: List[str], remove: List[str]):
View File
View File
-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)
-10
View File
@@ -1,10 +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)
-28
View File
@@ -1,28 +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 + "/db")
os.mkdir(dir + "/system")
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")
# 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
@@ -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)
-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)"
-380
View File
@@ -1,380 +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": {
"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"
}
}
}
}
},
"required": [
"name"
]
}
},
"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": {
"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"
}
},
"additionalProperties": false,
"required": [
"managed"
]
},
"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
}
-62
View File
@@ -1,62 +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
users:
- username: username
groups:
- group_1
home_dir: True
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:
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)"
-91
View File
@@ -1,91 +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):
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"
},
}
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
"""
# 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"))
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
+5 -30
View File
@@ -1,32 +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
# Ideas ## Init
- [ ] function to collect new configs - Copy
- [ ] 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]$))`
-8
View File
@@ -1,8 +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 file
TODO: Restructure this folder
-19
View File
@@ -1,19 +0,0 @@
from config.dtype.others import ArchMgrTemplateData
class ArchMgrTemplates:
pkg_bundles: dict[str, list[str]]
bootloader_config: BootLoaderSettings
theme_templates: ArchMgrTemplateData
repo_config: None # TODO: Define a good dtype here
class BootLoaderSettings:
templates: ArchMgrTemplateData
theme_folder: str
conf_build_cmd: str
name: str
def load_templates():
pass
-52
View File
@@ -1,52 +0,0 @@
hyprland:
- hyprland
- hyprlock
- hypridle
- grimblast
- grim
- xdg-desktop-portal-hyprland
- polkit-kde-agent
hyrpland-extra:
- hyprpaper
- wl-clipboard
- cliphist
- rofi
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
-7
View File
@@ -1,7 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"type": "object",
"properties": {
"": {}
}
}
+1
View File
@@ -0,0 +1 @@
import archmgr