Compare commits

...

25 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
janishutz 78eecfc81a feat(cli): Better organized CLI arg parsing 2026-05-14 17:36:06 +02:00
janishutz e24eb647ca feat(templates): More package templates, template management start 2026-05-12 14:15:18 +02:00
janishutz 72a8ceb741 feat(templates): pkg groups 2026-05-09 10:20:02 +02:00
janishutz ecb2952a7e feat(commit): individual packages from config respected 2026-05-07 18:25:19 +02:00
janishutz b7218c2a82 chore: some notes 2026-05-04 16:47:35 +02:00
janishutz 7b1dfe6ebc feat(config): Config loading and merging 2026-05-03 15:09:05 +02:00
janishutz 190fb86758 feat(typing): possibly complete 2026-05-03 14:24:03 +02:00
janishutz f9e6120910 feat(typing): More types, cleaner structure 2026-04-30 14:12:35 +02:00
janishutz 31426c006b feat(typing): Start adding python types for the configuration 2026-04-30 10:49:46 +02:00
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
51 changed files with 260 additions and 188 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
}
+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
+12 -8
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env python3 # The main app
# PYTHON_ARGCOMPLETE_OK from typing import cast
import cli as cliargs
import cli.args as cliargs
import commands.commit as commit import commands.commit as commit
import commands.config as config import commands.config as config
@@ -9,8 +8,10 @@ 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 import commands.prepare as setup
import commands.show as show
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:
@@ -29,19 +30,22 @@ if __name__ == "__main__":
\\___/ \\___/
""") """)
conf = {}
try: try:
if args.cmd == "commit": if args.cmd == "commit":
commit.commit(args.force, args.no_render) commit.commit(conf, args.force, args.no_render)
elif args.cmd == "config": elif args.cmd == "config":
config.config() config.config(args, conf)
elif args.cmd == "init": elif args.cmd == "init":
init.init(args.force) init.init(args.force)
elif args.cmd == "setup": elif args.cmd == "setup":
setup.setup() setup.setup()
elif args.cmd == "pull": elif args.cmd == "pull":
pull.pull(args.rebase, args.apply) pull.pull(conf, args.rebase, args.apply)
elif args.cmd == "push": elif args.cmd == "push":
push.push(args.force) push.push(args.force)
elif args.cmd == "show":
show.show(conf, cast(ArchMgrTemplates, {}), args)
except KeyboardInterrupt as e: except KeyboardInterrupt as e:
exit(130) exit(130)
except Exception as e: except Exception as e:
+34
View File
@@ -0,0 +1,34 @@
# CLI argument parsing and setup for it
import argparse
import argcomplete
from args import commit, init, pull, push, show
def add_cli_args():
ap = argparse.ArgumentParser(
"archmgr",
description="A nixos-like declarative config and package manager for Arch Linux (or any other distro, with some tweaks)",
usage="archmgr [command] [options]",
)
ap.add_argument("-v", "--version", action="version", version="%(prog)s V0.0.1")
sp = ap.add_subparsers(
title="commands",
metavar="Use 'archmgr [command] --help' to see help for each command",
dest="cmd",
)
# ┌ ┐
# │ Subcommands │
# └ ┘
sp.add_parser("setup", help="Do initial setup, like installing required tools")
commit.add_parser(sp)
sp.add_parser("config", help="prints information about your config")
init.add_parser(sp)
pull.add_parser(sp)
push.add_parser(sp)
show.add_parser(sp)
argcomplete.autocomplete(ap)
return ap.parse_args(), ap
+13
View File
@@ -0,0 +1,13 @@
import argparse as ap
def add_parser(sp: ap._SubParsersAction[ap.ArgumentParser]):
commit = sp.add_parser(
"commit", help="apply pending changes and commit to git repo"
)
commit.add_argument(
"-f", "--force", help="force apply (skips prompts)", action="store_true"
)
commit.add_argument(
"--no-render", "-r", help="do not re-render renderables", action="store_true"
)
+11
View File
@@ -0,0 +1,11 @@
import argparse as ap
def add_parser(sp: ap._SubParsersAction[ap.ArgumentParser]):
init = sp.add_parser("init", help="initialize a new archmgr repository")
init.add_argument(
"--force",
"-f",
help="resets the git repository and initializes new archmgr repo",
action="store_true",
)
+12
View File
@@ -0,0 +1,12 @@
import argparse as ap
def add_parser(sp: ap._SubParsersAction[ap.ArgumentParser]):
pull = sp.add_parser("pull", help="pull changes from git remote")
pull.add_argument("--rebase", "-r", help="execute rebase", action="store_true")
pull.add_argument(
"--apply",
"-a",
help="also apply the changes to the local system",
action="store_true",
)
+11
View File
@@ -0,0 +1,11 @@
import argparse as ap
def add_parser(sp: ap._SubParsersAction[ap.ArgumentParser]):
push = sp.add_parser("push", help="push pending changes to git remote")
push.add_argument(
"--force",
"-f",
help="force push (overriding possible remote changes)",
action="store_true",
)
+13
View File
@@ -0,0 +1,13 @@
import argparse as ap
def add_parser(sp: ap._SubParsersAction[ap.ArgumentParser]):
show = sp.add_parser("show", help="get information about configuration presets")
show_sp = show.add_subparsers(
title="config presets to show details on",
dest="show",
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.add_argument("show_pkg")
View File
View File
+1
View File
@@ -0,0 +1 @@
# Declare a file explicitly if it has different permissions
View File
+2
View File
@@ -0,0 +1,2 @@
def clone_git_repo(repo_url: str, clone_path: str, branch: str = "DEFAULT"):
pass
View File
+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
View File
+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
View File
+34
View File
@@ -0,0 +1,34 @@
from typing import List
def pkg_diff(target: List[str], actual: List[str]) -> tuple[List[str], List[str]]:
"""Compute a diff between target packages and installed packages
Args:
target: The target packages
actual: The actually installed packages
Returns:
A tuple with first the missing (not installed) packages
and second the extraneous (to be uninstalled) packages
"""
removed = []
diffed_out = []
for pkg in actual:
try:
diffed_out.append(target.index(pkg))
except Exception:
removed.append(pkg)
diffed_out.reverse()
new_pkgs = []
if len(diffed_out) > 0:
curr = diffed_out.pop()
for i, pkg in enumerate(target):
if i != curr:
new_pkgs.append(pkg)
else:
if len(diffed_out) > 0:
curr = diffed_out.pop()
return (new_pkgs, removed)
@@ -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
-61
View File
@@ -1,61 +0,0 @@
import argparse
import argcomplete
def add_cli_args():
ap = argparse.ArgumentParser(
"archmgr",
description="A nixos-like declarative config and package manager for Arch Linux (or any other distro, with some tweaks)",
usage="archmgr [command] [options]",
)
ap.add_argument("-v", "--version", action="version", version="%(prog)s V0.0.1")
sp = ap.add_subparsers(
title="commands",
metavar="Use 'archmgr [command] --help' to see help for each command",
dest="cmd",
)
# ┌ ┐
# │ Subcommands │
# └ ┘
commit = sp.add_parser(
"commit", help="apply pending changes and commit to git repo"
)
commit.add_argument("-f", "--force", help="force apply (skips prompts)", action="store_true")
commit.add_argument(
"--no-render", "-r", help="do not re-render renderables", action="store_true"
)
# 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.add_argument(
"--force",
"-f",
help="resets the git repository and initializes new archmgr repo",
action="store_true",
)
push = sp.add_parser("push", help="push pending changes to git remote")
push.add_argument(
"--force",
"-f",
help="force push (overriding possible remote changes)",
action="store_true",
)
pull = sp.add_parser("pull", help="pull changes from git remote")
pull.add_argument("--rebase", "-r", help="execute rebase", action="store_true")
pull.add_argument(
"--apply",
"-a",
help="also apply the changes to the local system",
action="store_true",
)
argcomplete.autocomplete(ap)
return ap.parse_args(), ap
-21
View File
@@ -1,21 +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
def commit(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([], pacman.list_explicitly_installed())
print_diff(add, remove)
if confirm(True, "Do you really want to proceed?"):
pw = password()
-4
View File
@@ -1,4 +0,0 @@
def config():
print("""
Your config can be found at
""")
-19
View File
@@ -1,19 +0,0 @@
import os
import commands.util.git as git
def init(force: bool = False):
dir = os.getcwd()
if force:
[os.remove(dir) for dir in os.listdir(dir)]
git.init(dir)
os.mkdir(dir + "/config")
os.mkdir(dir + "/etc")
with open(dir + "/config.yaml") as file:
file.write("")
print("Initialized a new archmgr repository")
# TODO: Warn user to not delete .config/archmgr repo
# TODO: Set up that repo (where to put it? /usr/share?)
# TODO: Consider collecting function
# 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!)
-28
View File
@@ -1,28 +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")
-2
View File
@@ -1,2 +0,0 @@
def pull(rebase: bool = False, apply: bool = False):
print("pull")
-2
View File
@@ -1,2 +0,0 @@
def push(force: bool = False):
print("push")
-22
View File
@@ -1,22 +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
"""
for i, pkg in enumerate(actual):
try:
idx = target.index(pkg)
target.pop(idx)
actual.pop(i)
except Exception:
pass
return (target, actual)
+5 -18
View File
@@ -1,20 +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
- Copy normal config files into correct directories
- 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
# 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
+1
View File
@@ -1,3 +1,4 @@
pyyaml
pylette pylette
argcomplete argcomplete
inquirer inquirer
+1
View File
@@ -0,0 +1 @@
import archmgr