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).
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
# PYTHON_ARGCOMPLETE_OK
import cli.args as cliargs
# The main app
from typing import cast
import cli as cliargs
import commands.commit as commit
import commands.config as config
@@ -9,8 +8,10 @@ import commands.init as init
import commands.pull as pull
import commands.push as push
import commands.prepare as setup
import commands.show as show
if __name__ == "__main__":
def run():
args, ap = cliargs.add_cli_args()
if args.cmd == None:
@@ -29,19 +30,22 @@ if __name__ == "__main__":
\\___/
""")
conf = {}
try:
if args.cmd == "commit":
commit.commit(args.force, args.no_render)
commit.commit(conf, args.force, args.no_render)
elif args.cmd == "config":
config.config()
config.config(args, conf)
elif args.cmd == "init":
init.init(args.force)
elif args.cmd == "setup":
setup.setup()
elif args.cmd == "pull":
pull.pull(args.rebase, args.apply)
pull.pull(conf, args.rebase, args.apply)
elif args.cmd == "push":
push.push(args.force)
elif args.cmd == "show":
show.show(conf, cast(ArchMgrTemplates, {}), args)
except KeyboardInterrupt as e:
exit(130)
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 commands.util.password_manager import PasswordManager
from password_mgr import PasswordManager
def choice(default: str, options: str, msg: str) -> str:
@@ -1,7 +1,7 @@
import subprocess as sp
from typing import List
from commands.util.input_mgr import password
from util.input import password
pkg_manager = ["yay", "--noconfirm"]
+2
View File
@@ -0,0 +1,2 @@
import list
import diff
@@ -1,7 +1,7 @@
from typing import List
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]):
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
- Create new commit (if possible)
- If force is unset: Diff system's files against current state by copying them into the current state branch
- 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
# Concepts
## Config
In python, using functions and args for them
# 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
## Init
- Copy
+1
View File
@@ -1,3 +1,4 @@
pyyaml
pylette
argcomplete
inquirer
+1
View File
@@ -0,0 +1 @@
import archmgr