From 4d3cf1552e00e5659ce365ce5ea609ce156aeea6 Mon Sep 17 00:00:00 2001 From: Rudra Bali Date: Wed, 18 Jan 2023 23:06:57 +0530 Subject: [PATCH 001/121] initial commit --- .gitignore | 4 + blend | 498 +++++++++++++++++++++++++++++++++++++++++++ completions/blend | 16 ++ debian/blend.install | 2 + debian/changelog | 5 + debian/compat | 1 + debian/control | 22 ++ debian/copyright | 24 +++ debian/rules | 4 + debian/source/format | 1 + 10 files changed, 577 insertions(+) create mode 100644 .gitignore create mode 100755 blend create mode 100755 completions/blend create mode 100644 debian/blend.install create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100755 debian/rules create mode 100644 debian/source/format diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e7fafc5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/debian/*.debhelper +/debian/*.substvars +/debian/debhelper-build-stamp +/debian/blend \ No newline at end of file diff --git a/blend b/blend new file mode 100755 index 0000000..28e0ee1 --- /dev/null +++ b/blend @@ -0,0 +1,498 @@ +#!/usr/bin/env python3 + +import os, sys +import shutil +import argparse +import subprocess + +__version = '1.0.0' + +### Colors +class colors: + reset = '\033[0m' + bold = '\033[01m' + disable = '\033[02m' + underline = '\033[04m' + reverse = '\033[07m' + strikethrough = '\033[09m' + invisible = '\033[08m' + + class fg: + black = '\033[30m' + red = '\033[31m' + green = '\033[32m' + orange = '\033[33m' + blue = '\033[34m' + purple = '\033[35m' + cyan = '\033[36m' + lightgrey = '\033[37m' + darkgrey = '\033[90m' + lightred = '\033[91m' + lightgreen = '\033[92m' + yellow = '\033[93m' + lightblue = '\033[94m' + pink = '\033[95m' + lightcyan = '\033[96m' + + class bg: + black = '\033[40m' + red = '\033[41m' + green = '\033[42m' + orange = '\033[43m' + blue = '\033[44m' + purple = '\033[45m' + cyan = '\033[46m' + lightgrey = '\033[47m' + +### END + +### Helper functions + +def info(msg): + print (colors.bold + colors.fg.cyan + '>> i: ' + colors.reset + colors.bold + msg + colors.reset) + +def error(err): + print (colors.bold + colors.fg.red + '>> e: ' + colors.reset + colors.bold + err + colors.reset) + +### END + +distro_map = { + 'arch': 'docker.io/library/archlinux', + 'fedora-rawhide': 'fedora:rawhide', + 'ubuntu-22.04': 'ubuntu:22.04', + 'ubuntu-22.10': 'ubuntu:22.10' +} + +default_distro = 'arch' + +def get_distro(): + try: + return distro_map[args.distro] + except: + error(f"{args.distro} isn't supported by blend.") + exit() + +def distrobox_list_containers(): + _list = subprocess.run(['podman', 'ps', '-a', '--no-trunc', '--size', '--format', + '{{.Names}}:{{.Mounts}}'], stdout=subprocess.PIPE).stdout.decode('utf-8').strip() + if len(_list) == 0: + info('No containers. Create one by installing a package (`blend install hello`), or manually create one (`blend create-container arch`).') + for container in _list.splitlines(keepends=False): + if 'distrobox' in container.split(':')[1]: + print(container.split(':')[0]) + return False + +def distrobox_check_container(name): + _list = subprocess.run(['podman', 'ps', '-a', '--no-trunc', '--size', '--format', + '{{.Names}}:{{.Mounts}}'], stdout=subprocess.PIPE).stdout.decode('utf-8').strip() + for container in _list.splitlines(keepends=False): + if 'distrobox' in container.split(':')[1] and name.strip() == container.split(':')[0]: + return True + return False + +def distrobox_create_container(): + name = args.container_name + distro = args.distro + info(f'creating container {name}, using {distro}') + if subprocess.run(['distrobox-create', '-Y', '-n', name, '-i', get_distro()], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0: + if distrobox_check_container(name): + error(f'container {name} already exists') + exit(1) + error(f'failed to create container {name}') + exit(1) + subprocess.call(['distrobox-enter', '-nw', '-n', args.container_name, '--', 'echo -n']) + if distro == 'arch': + distrobox_run_container('sudo pacman -Sy') + distrobox_run_container('sudo pacman --noconfirm -S --needed git base-devel') + distrobox_run_container('cd ~; git clone https://aur.archlinux.org/yay.git') + distrobox_run_container('cd ~/yay; makepkg --noconfirm -si') + +distrobox_get_output = lambda cmd: subprocess.run(['distrobox-enter', '-nw', '-n', args.container_name, '--', cmd], + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode('UTF-8').strip() + +def distrobox_run_container(cmd): + subprocess.call(['podman', 'exec', '-u', os.environ['USER'], '-it', args.container_name, 'sh', '-c', cmd]) + +def distrobox_install_pkg(pkg): + if args.distro == 'fedora-rawhide': + if args.noconfirm == True: + distrobox_run_container(f'sudo dnf -y install {pkg}') + else: + distrobox_run_container(f'sudo dnf install {pkg}') + elif args.distro == 'arch': + if distrobox_get_output('yay --version') == '': + distrobox_run_container('sudo pacman -Sy') + distrobox_run_container('sudo pacman --noconfirm -S --needed git base-devel') + distrobox_run_container('git clone https://aur.archlinux.org/yay.git') + distrobox_run_container('cd yay; makepkg --noconfirm -si') + distrobox_run_container(f'yay -Sy') + if args.noconfirm == True: + distrobox_run_container(f'yay --noconfirm -S {pkg}') + else: + distrobox_run_container(f'yay -S {pkg}') + elif args.distro.startswith('ubuntu-'): + distrobox_run_container(f'sudo apt-get update') + if args.noconfirm == True: + distrobox_run_container(f'sudo apt-get install -y {pkg}') + else: + distrobox_run_container(f'sudo apt-get install {pkg}') + +def distrobox_remove_pkg(pkg): + if args.distro == 'fedora-rawhide': + if args.noconfirm == True: + distrobox_run_container(f'sudo dnf -y remove {pkg}') + else: + distrobox_run_container(f'sudo dnf remove {pkg}') + elif args.distro == 'arch': + if args.noconfirm == True: + distrobox_run_container(f'sudo pacman --noconfirm -Rcns {pkg}') + else: + distrobox_run_container(f'sudo pacman -Rcns {pkg}') + elif args.distro.startswith('ubuntu-'): + if args.noconfirm == True: + distrobox_run_container(f'sudo apt-get purge -y {pkg}') + else: + distrobox_run_container(f'sudo apt-get purge {pkg}') + distrobox_run_container(f'sudo apt-get autoremove --purge -y {pkg}') + +def distrobox_search_pkg(pkg): + if args.distro == 'fedora-rawhide': + distrobox_run_container(f'dnf search {pkg}') + elif args.distro == 'arch': + distrobox_run_container(f'yay -Sy') + distrobox_run_container(f'yay {pkg}') + elif args.distro.startswith('ubuntu-'): + distrobox_run_container(f'sudo apt-get update') + distrobox_run_container(f'apt-cache search {pkg}') + +def distrobox_show_pkg(pkg): + if args.distro == 'fedora-rawhide': + distrobox_run_container(f'dnf info {pkg}') + elif args.distro == 'arch': + distrobox_run_container(f'yay -Sy') + distrobox_run_container(f'yay -Si {pkg}') + elif args.distro.startswith('ubuntu-'): + distrobox_run_container(f'sudo apt-get update') + distrobox_run_container(f'apt-cache show {pkg}') + +def install_de(): + if len(args.pkg) == 0: + error('you need to specify a desktop environment (gnome and mate are supported)') + exit() + name = args.pkg[0] + if name == 'mate' or name == 'gnome': + if distro_input == None and cn_input == None: + info(f'using fedora-rawhide instead of {default_distro}, as it\'s recommended for {name}') + info('if you want to use arch, you can specify `--distro arch`') + args.distro = 'fedora-rawhide' + args.container_name = 'fedora-rawhide' + else: + error(f'desktop environment {name} is not supported') + info('gnome and mate are supported') + exit() + if not distrobox_check_container(args.container_name): + distrobox_create_container() + info(f'installing {name} on {args.container_name} (using {args.distro})') + print() + info('ignore any errors after this') + distrobox_run_container('sudo umount /run/systemd/system') + distrobox_run_container('sudo rmdir /run/systemd/system') + distrobox_run_container('sudo ln -s /run/host/run/systemd/system /run/systemd &> /dev/null') + distrobox_run_container('sudo ln -s /run/host/run/dbus/system_bus_socket /run/dbus/ &> /dev/null') + distrobox_run_container('sudo mkdir -p /usr/local/bin') + distrobox_run_container('echo "#!/usr/bin/env bash" | sudo tee /usr/local/bin/disable_dbusactivatable_blend') + distrobox_run_container('echo "if [[ \$DISABLE_REPEAT -ne 1 ]]; then" | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend') + distrobox_run_container('''echo 'while true; do for dir in ${XDG_DATA_DIRS//:/ }; do sudo find $dir/applications -type f -exec sed -i -e '"'s/DBusActivatable=.*//g'"' {} '"';'"'; done; sleep 5; done' | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend''') + distrobox_run_container('echo "else" | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend') + distrobox_run_container('''echo 'for dir in ${XDG_DATA_DIRS//:/ }; do sudo find $dir/applications -type f -exec sed -i -e '"'s/DBusActivatable=.*//g'"' {} '"';'"'; done' | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend''') + distrobox_run_container('echo "fi" | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend') + distrobox_run_container('sudo chmod 755 /usr/local/bin/disable_dbusactivatable_blend') + distrobox_run_container('sudo mkdir -p /etc/xdg/autostart') + distrobox_run_container('echo "[Desktop Entry]" | sudo tee /usr/share/applications/disable_dbusactivatable_blend.desktop') + distrobox_run_container('echo "Version=1.0" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') + distrobox_run_container('echo "Name=Remove DBusActivatable (blend)" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') + distrobox_run_container('echo "Comment=Remove DBusActivatable in all desktop files" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') + distrobox_run_container('echo "Exec=disable_dbusactivatable_blend" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') + distrobox_run_container('echo "Type=Application" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') + distrobox_run_container('sudo chmod 755 /usr/share/applications/disable_dbusactivatable_blend.desktop') + if name == 'mate': + if args.distro == 'fedora-rawhide': + if args.noconfirm == True: + distrobox_run_container(f'sudo dnf --allowerasing -y groupinstall MATE') + else: + distrobox_run_container(f'sudo dnf --allowerasing groupinstall MATE') + elif args.distro == 'arch': + if args.noconfirm == True: + distrobox_run_container(f'sudo pacman --noconfirm -S mate mate-extra') + else: + distrobox_run_container(f'sudo pacman -S mate mate-extra') + elif args.distro.startswith('ubuntu-'): + distrobox_run_container(f'sudo apt-get update') + if args.noconfirm == True: + distrobox_run_container(f'sudo apt-get install -y ubuntu-mate-desktop') + else: + distrobox_run_container(f'sudo apt-get install ubuntu-mate-desktop') + subprocess.run('sudo mkdir -p /usr/local/bin', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "#!/usr/bin/env sh" | sudo tee /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "export GTK_MODULES=" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "export GTK3_MODULES=" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "export LD_PRELOAD=" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "chown -f -R $USER:$USER /tmp/.X11-unix" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "distrobox-enter {args.container_name} -- mate-session" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'sudo chmod 755 /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "[Desktop Entry]" | sudo tee /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Version=1.0" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Name=MATE ({args.container_name})" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Comment=Use this session to run MATE as your desktop environment" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Exec={args.container_name}-mate-blend" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Type=Application" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'sudo chmod 755 /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + elif name == 'gnome': + if args.distro == 'fedora-rawhide': + if args.noconfirm == True: + distrobox_run_container(f'sudo dnf --allowerasing -y groupinstall GNOME') + distrobox_run_container(f'sudo dnf --allowerasing -y remove gnome-terminal') + distrobox_run_container(f'sudo dnf --allowerasing -y install gnome-console') + else: + distrobox_run_container(f'sudo dnf --allowerasing groupinstall GNOME') + distrobox_run_container(f'sudo dnf --allowerasing -y remove gnome-terminal') + distrobox_run_container(f'sudo dnf --allowerasing -y install gnome-console') + elif args.distro == 'arch': + if args.noconfirm == True: + distrobox_run_container(f'sudo pacman --noconfirm -S gnome gnome-console') + distrobox_run_container(f'sudo pacman --noconfirm -Rcns gnome-terminal') + else: + distrobox_run_container(f'sudo pacman -S gnome gnome-console') + distrobox_run_container(f'sudo pacman --noconfirm -Rcns gnome-terminal') + elif args.distro.startswith('ubuntu-'): + distrobox_run_container(f'sudo apt-get update') + if args.noconfirm == True: + distrobox_run_container(f'sudo apt-get install -y ubuntu-desktop gnome-console') + distrobox_run_container(f'sudo apt-get purge -y gnome-terminal') + distrobox_run_container(f'sudo apt-get purge -y --auto-remove') + else: + distrobox_run_container(f'sudo apt-get install ubuntu-desktop gnome-console') + distrobox_run_container(f'sudo apt-get purge -y gnome-terminal') + distrobox_run_container(f'sudo apt-get purge -y --auto-remove') + subprocess.run(f'echo "[Desktop Entry]" | sudo tee /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Name=GNOME on Wayland ({args.container_name})" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Comment=This session logs you into GNOME" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Exec=distrobox-enter -n {args.container_name} -- gnome-session --builtin" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Type=Application" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "DesktopNames=GNOME" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "X-GDM-SessionRegisters=true" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'sudo chmod 755 /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + +def copy_desktop_files(pkg): + distrobox_run_container('sudo mkdir -p /usr/share/applications') + desktop_files = distrobox_get_output("find /usr/share/applications -type f -printf '%f\\n'").split('\r\n') + desktop_files[:] = [f.removesuffix('.desktop') for f in desktop_files if '.desktop' in f] + for f in desktop_files: + distrobox_run_container(f'CONTAINER_ID={args.container_name} distrobox-export --app {f}') + +def install_blend(): + if len(args.pkg) != 0: + info('installed binaries can be executed from each container\'s respective terminal') + print() + + if len(args.pkg) == 0: + error('no packages to install') + + for pkg in args.pkg: + info(f'installing blend {pkg}') + if not distrobox_check_container(args.container_name): + distrobox_create_container() + distrobox_install_pkg(pkg) + copy_desktop_files(pkg) + +def remove_blend(): + if len(args.pkg) == 0: + error('no packages to remove') + + for pkg in args.pkg: + info(f'removing blend {pkg}') + if not distrobox_check_container(args.container_name): + error(f"container {args.container_name} doesn't exist") + distrobox_remove_pkg(pkg) + subprocess.run(f'rm -f ~/.local/share/applications/{args.container_name}-*{pkg}*.desktop', shell=True) + +def search_blend(): + if len(args.pkg) == 0: + error('no packages to search for') + + for pkg in args.pkg: + if not distrobox_check_container(args.container_name): + error(f"container {args.container_name} doesn't exist") + distrobox_search_pkg(pkg) + +def show_blend(): + if len(args.pkg) == 0: + error('no packages to show') + + for pkg in args.pkg: + info(f'info about blend {pkg}') + if not distrobox_check_container(args.container_name): + error(f"container {args.container_name} doesn't exist") + distrobox_show_pkg(pkg) + +def sync_blends(): + if args.distro == 'fedora-rawhide': + distrobox_run_container(f'dnf makecache') + elif args.distro == 'arch': + distrobox_run_container(f'yay -Syy') + elif args.distro.startswith('ubuntu-'): + distrobox_run_container(f'sudo apt-get update') + +def update_blends(): + if args.distro == 'fedora-rawhide': + if args.noconfirm == True: + distrobox_run_container(f'sudo dnf -y upgrade') + else: + distrobox_run_container(f'sudo dnf upgrade') + elif args.distro == 'arch': + if args.noconfirm == True: + distrobox_run_container(f'yay --noconfirm') + else: + distrobox_run_container(f'yay') + elif args.distro.startswith('ubuntu-'): + distrobox_run_container(f'sudo apt-get update') + if args.noconfirm == True: + distrobox_run_container(f'sudo apt-get dist-upgrade -y') + else: + distrobox_run_container(f'sudo apt-get dist-upgrade') + else: + error(f'distribution {args.distro} is not supported') + +def enter_container(): + if len(args.pkg) == 0: + error(f'you need to specify a container (run `blend list-container` to list all the containers)') + exit() + container = args.pkg[0] + info(f'entering container {container}') + args.container_name = container + subprocess.call(['distrobox-enter', '-n', args.container_name]) + +def create_container(): + for container in args.pkg: + args.container_name = container + distrobox_create_container() + subprocess.call(['distrobox-enter', '-nw', '-n', args.container_name, '--', 'echo -n']) + +def remove_container(): + for container in args.pkg: + info(f'removing container {container}') + subprocess.run(['podman', 'stop', container], stdout=subprocess.DEVNULL) + subprocess.run(['distrobox-rm', container, '--force'], stdout=subprocess.DEVNULL) + +def start_containers(): + _list = subprocess.run(['podman', 'ps', '-a', '--no-trunc', '--size', '--format', + '{{.Names}}:{{.Mounts}}'], stdout=subprocess.PIPE).stdout.decode('utf-8').strip() + if len(_list) == 0: + info('No containers. Create one by installing a package (`blend install hello`), or manually create one (`blend create-container arch`).') + for container in _list.splitlines(keepends=False): + container = container.split(':')[0] + info(f'starting container {container}') + subprocess.call(['distrobox-enter', '-n', args.container_name, '--', 'true']) + +if shutil.which('distrobox') is None: + error("distrobox isn't installed, which is a hard dep") + exit(1) + +if os.geteuid() == 0: + error("do not run as root") + exit(1) + +description = f''' +{colors.bold}{colors.fg.purple}Usage:{colors.reset} + blend [command] [options] [arguments] + +{colors.bold}{colors.fg.purple}Version:{colors.reset} {__version}{colors.bold} + +{colors.bold}{colors.bg.purple}blend{colors.reset}{colors.bold} is a package manager for {colors.bg.purple}blendOS{colors.reset}{colors.bold}, which includes support for Arch, Ubuntu and Fedora packages.{colors.reset} + +{colors.bold}{colors.fg.purple}default distro{colors.reset}: {colors.bold}{colors.fg.lightblue}arch{colors.reset} (default container's name is the same as that of the default distro) + +Here's a list of the supported distros: +{colors.bold}1.{colors.reset} arch +{colors.bold}2.{colors.reset} fedora-rawhide +{colors.bold}3.{colors.reset} ubuntu-22.04 +{colors.bold}4.{colors.reset} ubuntu-22.10 +(debian support is coming soon) + +You can use any of these distros by passing the option {colors.bold}--distro=[NAME OF THE DISTRO]{colors.reset}. + +{colors.bold}{colors.fg.lightblue}arch{colors.reset} also supports AUR packages, for an extremely large app catalog. + +{colors.bold}{colors.fg.purple}available commands{colors.reset}: + {colors.bold}help{colors.reset} Show this help message and exit. + {colors.bold}version{colors.reset} Show version information and exit. + {colors.bold}enter{colors.reset} Enter the container shell. + {colors.bold}install{colors.reset} Install packages inside a container. + {colors.bold}remove{colors.reset} Remove packages inside a managed container. + {colors.bold}create-container{colors.reset} Create a container managed by blend. + {colors.bold}remove-container{colors.reset} Remove a container managed by blend. + {colors.bold}list-containers{colors.reset} List all the containers managed by blend. + {colors.bold}start-containers{colors.reset} Start all the container managed by blend. + {colors.bold}sync{colors.reset} Sync list of available packages from repository. + {colors.bold}search{colors.reset} Search for packages in a managed container. + {colors.bold}show{colors.reset} Show details about a package. + {colors.bold}update{colors.reset} Update all the packages in a managed container. + +{colors.bold}{colors.fg.purple}options for commands{colors.reset}: + {colors.bold}-cn CONTAINER NAME, --container-name CONTAINER NAME{colors.reset} + set the container name (the default is the name of the distro) + {colors.bold}-d DISTRO, --distro DISTRO{colors.reset} + set the distro name (supported: arch fedora-rawhide ubuntu-22.04 ubuntu-22.10; default is arch) + {colors.bold}-y, --noconfirm{colors.reset} assume yes for all questions + {colors.bold}-v, --version{colors.reset} show version information and exit +''' + +epilog = f''' +{colors.bold}Made with {colors.fg.red}\u2764{colors.reset}{colors.bold} by Rudra Saraswat.{colors.reset} +''' + +parser = argparse.ArgumentParser(description=description, usage=argparse.SUPPRESS, + epilog=epilog, formatter_class=argparse.RawTextHelpFormatter) +command_map = { 'install': install_blend, + 'remove': remove_blend, + 'install-de': install_de, + 'enter': enter_container, + 'create-container': create_container, + 'remove-container': remove_container, + 'list-containers': distrobox_list_containers, + 'start-containers': start_containers, + 'sync': sync_blends, + 'update': update_blends, + 'search': search_blend, + 'show': show_blend, + 'help': 'help', + 'version': 'version' } +parser.add_argument('command', choices=command_map.keys(), help=argparse.SUPPRESS) +parser.add_argument('pkg', action='store', type=str, nargs='*', help=argparse.SUPPRESS) +parser.add_argument('-cn', '--container-name', action='store', nargs=1, metavar='CONTAINER NAME', help=argparse.SUPPRESS) +parser.add_argument('-y', '--noconfirm', action='store_true', help=argparse.SUPPRESS) +parser.add_argument('-d', '--distro', action='store', nargs=1, metavar='DISTRO', help=argparse.SUPPRESS) +parser.add_argument('-v', '--version', action='version', version=f'%(prog)s {__version}', help=argparse.SUPPRESS) + +if len(sys.argv) == 1: + parser.print_help() + exit() + +args = parser.parse_intermixed_args() + +distro_input = args.distro +args.distro = default_distro if args.distro == None else args.distro[0] +cn_input = args.container_name +args.container_name = args.distro if args.container_name == None else args.container_name[0] + +if args.container_name in distro_map and not args.distro == args.container_name and distro_input == None: + args.distro = args.container_name + info(f'assuming you meant to use {args.distro} as the distro') + +command = command_map[args.command] +if command == 'help': + parser.print_help() +elif command == 'version': + parser.parse_args(['--version']) +else: + command() \ No newline at end of file diff --git a/completions/blend b/completions/blend new file mode 100755 index 0000000..527dd6d --- /dev/null +++ b/completions/blend @@ -0,0 +1,16 @@ +#/usr/bin/env bash + +set +e + +_completions() { + SUGGESTIONS=() + + if [[ "$COMP_CWORD" == 1 ]]; then + SUGGESTIONS=('install' 'remove' 'update' 'show' 'search' 'enter' 'create-container' 'remove-container' \ + 'list-container' 'start-containers' 'sync' 'help' 'version' ) + fi + + COMPREPLY=($(compgen -W "${SUGGESTIONS[*]}" "${COMP_WORDS[$COMP_CWORD]}")) +} + +complete -F _completions -- blend \ No newline at end of file diff --git a/debian/blend.install b/debian/blend.install new file mode 100644 index 0000000..3888acc --- /dev/null +++ b/debian/blend.install @@ -0,0 +1,2 @@ +blend /usr/bin/ +completions/blend /usr/share/bash-completion/completions/ \ No newline at end of file diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..18cdff8 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +blend (1.0.0) lunar; urgency=medium + + * First Release + + -- Rudra Saraswat Wed, 18 Jan 2023 21:58:53 +0200 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..9d60796 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +11 \ No newline at end of file diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..30b7544 --- /dev/null +++ b/debian/control @@ -0,0 +1,22 @@ +Source: blend +Section: devel +Priority: optional +Maintainer: Rudra Saraswat +Build-Depends: debhelper (>= 9), +Standards-Version: 3.9.6 +Homepage: https://github.com/blend-os/blend +Vcs-Browser: https://github.com/blend-os/blend +Vcs-Git: git://github.com/blend-os/blend.git + +Package: blend +Architecture: all +Depends: ${shlibs:Depends}, + ${misc:Depends}, + distrobox, + podman, + python3 +Breaks: mrtrix3 +Built-Using: ${misc:Built-Using} +Description: Package manager for blendOS. + This package contains the package manger for + blendOS. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..f81f27e --- /dev/null +++ b/debian/copyright @@ -0,0 +1,24 @@ +Format: http://dep.debian.net/deps/dep5 +Upstream-Name: Blend OS First Setup +Source: https://github.com/blend-os/ + +Files: * +Copyright: 2023 blendOS contributors + 2022 Vanilla-OS contributors +License: GPL-3.0 + +License: GPL-3.0 + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License. + . + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see . + . + On Debian systems, the complete text of the GNU General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". \ No newline at end of file diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..2d33f6a --- /dev/null +++ b/debian/rules @@ -0,0 +1,4 @@ +#!/usr/bin/make -f + +%: + dh $@ diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..9f67427 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) \ No newline at end of file From bdd929e487bd08ba41676fc921c859f4e41890a0 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Wed, 18 Jan 2023 23:10:18 +0530 Subject: [PATCH 002/121] Create LICENSE --- LICENSE | 674 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 674 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. From e2eee152acba930f0cc5eb40369ff67454c20435 Mon Sep 17 00:00:00 2001 From: Rudra Bali Date: Wed, 18 Jan 2023 23:17:47 +0530 Subject: [PATCH 003/121] release 1.0.1 --- README.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++ blend | 10 ++++++- completions/blend | 2 +- debian/changelog | 6 ++++ 4 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..a768d01 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +
+

blend

+

A package manager for blendOS

+
+ +# almost +On-demand immutability for blendOS. + +This was originally developed by VanillaOS. + +> **Note**: This is a work in progress. It is not ready for production use. + +### Help +``` +Usage: + blend [command] [options] [arguments] + +Version: 1.0.1 + +blend is a package manager for blendOS, which includes support for Arch, Ubuntu and Fedora packages. + +default distro: arch (default container's name is the same as that of the default distro) + +Here's a list of the supported distros: +1. arch +2. fedora-rawhide +3. ubuntu-22.04 +4. ubuntu-22.10 +(debian support is coming soon) + +You can use any of these distros by passing the option --distro=[NAME OF THE DISTRO]. + +You can even install a supported desktop environment in a blend container (run `blend install-de [DESKTOP ENVIRONMENT NAME]` to install your favorite desktop environment). + +Here's a list of the supported desktop environments: +1. gnome +2. mate +(support for many more DEs is coming soon) + +arch also supports AUR packages, for an extremely large app catalog. + +available commands: + help Show this help message and exit. + version Show version information and exit. + enter Enter the container shell. + install Install packages inside a container. + install-de Install a desktop environment inside a container. + remove Remove packages inside a managed container. + create-container Create a container managed by blend. + remove-container Remove a container managed by blend. + list-containers List all the containers managed by blend. + start-containers Start all the container managed by blend. + sync Sync list of available packages from repository. + search Search for packages in a managed container. + show Show details about a package. + update Update all the packages in a managed container. + +options for commands: + -cn CONTAINER NAME, --container-name CONTAINER NAME + set the container name (the default is the name of the distro) + -d DISTRO, --distro DISTRO + set the distro name (supported: arch fedora-rawhide ubuntu-22.04 ubuntu-22.10; default is arch) + -y, --noconfirm assume yes for all questions + -v, --version show version information and exit + +options: + -h, --help show this help message and exit + +Made with ❤ by Rudra Saraswat. +``` diff --git a/blend b/blend index 28e0ee1..8ddb861 100755 --- a/blend +++ b/blend @@ -5,7 +5,7 @@ import shutil import argparse import subprocess -__version = '1.0.0' +__version = '1.0.1' ### Colors class colors: @@ -421,6 +421,13 @@ Here's a list of the supported distros: You can use any of these distros by passing the option {colors.bold}--distro=[NAME OF THE DISTRO]{colors.reset}. +You can even install a supported desktop environment in a blend container (run `blend install-de [DESKTOP ENVIRONMENT NAME]` to install your favorite desktop environment). + +Here's a list of the supported desktop environments: +{colors.bold}1.{colors.reset} gnome +{colors.bold}2.{colors.reset} mate +(support for many more DEs is coming soon) + {colors.bold}{colors.fg.lightblue}arch{colors.reset} also supports AUR packages, for an extremely large app catalog. {colors.bold}{colors.fg.purple}available commands{colors.reset}: @@ -428,6 +435,7 @@ You can use any of these distros by passing the option {colors.bold}--distro=[NA {colors.bold}version{colors.reset} Show version information and exit. {colors.bold}enter{colors.reset} Enter the container shell. {colors.bold}install{colors.reset} Install packages inside a container. + {colors.bold}install-de{colors.reset} Install a desktop environment inside a container. {colors.bold}remove{colors.reset} Remove packages inside a managed container. {colors.bold}create-container{colors.reset} Create a container managed by blend. {colors.bold}remove-container{colors.reset} Remove a container managed by blend. diff --git a/completions/blend b/completions/blend index 527dd6d..70bbdee 100755 --- a/completions/blend +++ b/completions/blend @@ -7,7 +7,7 @@ _completions() { if [[ "$COMP_CWORD" == 1 ]]; then SUGGESTIONS=('install' 'remove' 'update' 'show' 'search' 'enter' 'create-container' 'remove-container' \ - 'list-container' 'start-containers' 'sync' 'help' 'version' ) + 'install-de' 'list-container' 'start-containers' 'sync' 'help' 'version' ) fi COMPREPLY=($(compgen -W "${SUGGESTIONS[*]}" "${COMP_WORDS[$COMP_CWORD]}")) diff --git a/debian/changelog b/debian/changelog index 18cdff8..e89874f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +blend (1.0.1) lunar; urgency=medium + + * Sync with blend 1.0.1 + + -- Rudra Bali Wed, 18 Jan 2023 23:14:32 +0530 + blend (1.0.0) lunar; urgency=medium * First Release From 6eff7a1c099563fc3b40265bcf6779e8d6f31d98 Mon Sep 17 00:00:00 2001 From: Rudra Bali Date: Thu, 19 Jan 2023 14:31:12 +0530 Subject: [PATCH 004/121] bump up to 1.0.3 --- .gitignore | 3 +- README.md | 8 +++-- blend | 69 ++++++++++++++++++++++++++++--------------- blend-pkgmngr-path.sh | 9 ++++++ completions/blend | 5 ++-- debian/blend.install | 2 ++ debian/changelog | 12 ++++++++ pkgmanagers/apt | 10 +++++++ pkgmanagers/apt-get | 10 +++++++ pkgmanagers/dnf | 10 +++++++ pkgmanagers/pacman | 10 +++++++ pkgmanagers/yay | 10 +++++++ pkgmanagers/yum | 10 +++++++ 13 files changed, 139 insertions(+), 29 deletions(-) create mode 100644 blend-pkgmngr-path.sh create mode 100755 pkgmanagers/apt create mode 100755 pkgmanagers/apt-get create mode 100755 pkgmanagers/dnf create mode 100755 pkgmanagers/pacman create mode 100755 pkgmanagers/yay create mode 100755 pkgmanagers/yum diff --git a/.gitignore b/.gitignore index e7fafc5..41a53bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /debian/*.debhelper /debian/*.substvars /debian/debhelper-build-stamp -/debian/blend \ No newline at end of file +/debian/blend +/debian/files diff --git a/README.md b/README.md index a768d01..359c6f4 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ This was originally developed by VanillaOS. Usage: blend [command] [options] [arguments] -Version: 1.0.1 +Version: 1.0.3 blend is a package manager for blendOS, which includes support for Arch, Ubuntu and Fedora packages. @@ -32,6 +32,8 @@ You can use any of these distros by passing the option --distro=[NAME OF THE DIS You can even install a supported desktop environment in a blend container (run `blend install-de [DESKTOP ENVIRONMENT NAME]` to install your favorite desktop environment). +However, this feature is still somewhat experimental, and some apps might be buggy. + Here's a list of the supported desktop environments: 1. gnome 2. mate @@ -43,8 +45,10 @@ available commands: help Show this help message and exit. version Show version information and exit. enter Enter the container shell. + export Export the desktop entry for an installed blend. + unexport Unexport the desktop entry for a blend. install Install packages inside a container. - install-de Install a desktop environment inside a container. + install-de Install a desktop environment inside a container. (EXPERIMENTAL) remove Remove packages inside a managed container. create-container Create a container managed by blend. remove-container Remove a container managed by blend. diff --git a/blend b/blend index 8ddb861..9b9cbe9 100755 --- a/blend +++ b/blend @@ -5,7 +5,7 @@ import shutil import argparse import subprocess -__version = '1.0.1' +__version = '1.0.3' ### Colors class colors: @@ -77,6 +77,8 @@ def distrobox_list_containers(): '{{.Names}}:{{.Mounts}}'], stdout=subprocess.PIPE).stdout.decode('utf-8').strip() if len(_list) == 0: info('No containers. Create one by installing a package (`blend install hello`), or manually create one (`blend create-container arch`).') + else: + info('List of containers:') for container in _list.splitlines(keepends=False): if 'distrobox' in container.split(':')[1]: print(container.split(':')[0]) @@ -107,7 +109,7 @@ def distrobox_create_container(): distrobox_run_container('cd ~; git clone https://aur.archlinux.org/yay.git') distrobox_run_container('cd ~/yay; makepkg --noconfirm -si') -distrobox_get_output = lambda cmd: subprocess.run(['distrobox-enter', '-nw', '-n', args.container_name, '--', cmd], +distrobox_get_output = lambda cmd: subprocess.run(['distrobox-enter', '-nw', '-n', args.container_name, '--', 'sh', '-c', cmd], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode('UTF-8').strip() def distrobox_run_container(cmd): @@ -275,20 +277,37 @@ def install_de(): distrobox_run_container(f'sudo apt-get purge -y gnome-terminal') distrobox_run_container(f'sudo apt-get purge -y --auto-remove') subprocess.run(f'echo "[Desktop Entry]" | sudo tee /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Name=GNOME on Wayland ({args.container_name})" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Name=GNOME on Wayland ({args.container_name})" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) subprocess.run(f'echo "Comment=This session logs you into GNOME" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) subprocess.run(f'echo "Exec=distrobox-enter -n {args.container_name} -- gnome-session --builtin" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) subprocess.run(f'echo "Type=Application" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) subprocess.run(f'echo "DesktopNames=GNOME" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) subprocess.run(f'echo "X-GDM-SessionRegisters=true" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "[Desktop Entry]" | sudo tee /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Name=GNOME on Xorg ({args.container_name})" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Comment=This session logs you into GNOME" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Exec=distrobox-enter -n {args.container_name} -- gnome-session --builtin" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Type=Application" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "DesktopNames=GNOME" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "X-GDM-SessionRegisters=true" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) subprocess.run(f'sudo chmod 755 /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'sudo chmod 755 /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + distrobox_run_container('sudo mkdir -p /usr/share/applications /usr/local/share/applications') + distrobox_run_container('''echo 'for dir in /usr/share/applications /usr/local/share/applications; do sudo find $dir -type f -exec sed -i -e '"'s/DBusActivatable=.*//g'"' {} '"';'"'; done' | sudo bash''') def copy_desktop_files(pkg): - distrobox_run_container('sudo mkdir -p /usr/share/applications') - desktop_files = distrobox_get_output("find /usr/share/applications -type f -printf '%f\\n'").split('\r\n') - desktop_files[:] = [f.removesuffix('.desktop') for f in desktop_files if '.desktop' in f] - for f in desktop_files: - distrobox_run_container(f'CONTAINER_ID={args.container_name} distrobox-export --app {f}') + distrobox_run_container(f'CONTAINER_ID={args.container_name} distrobox-export --app {pkg} &>/dev/null') + +def del_desktop_files(pkg): + distrobox_run_container(f'CONTAINER_ID={args.container_name} distrobox-export --app {pkg} --delete &>/dev/null') + +def export_blend(): + for pkg in args.pkg: + copy_desktop_files(pkg) + +def unexport_blend(): + for pkg in args.pkg: + del_desktop_files(pkg) def install_blend(): if len(args.pkg) != 0: @@ -314,7 +333,7 @@ def remove_blend(): if not distrobox_check_container(args.container_name): error(f"container {args.container_name} doesn't exist") distrobox_remove_pkg(pkg) - subprocess.run(f'rm -f ~/.local/share/applications/{args.container_name}-*{pkg}*.desktop', shell=True) + del_desktop_files(pkg) def search_blend(): if len(args.pkg) == 0: @@ -364,13 +383,12 @@ def update_blends(): error(f'distribution {args.distro} is not supported') def enter_container(): - if len(args.pkg) == 0: - error(f'you need to specify a container (run `blend list-container` to list all the containers)') - exit() - container = args.pkg[0] - info(f'entering container {container}') - args.container_name = container - subprocess.call(['distrobox-enter', '-n', args.container_name]) + if not distrobox_check_container(args.container_name): + distrobox_create_container() + if os.environ.get('BLEND_COMMAND') == None or os.environ.get('BLEND_COMMAND') == '': + exit(subprocess.call(['distrobox-enter', '-n', args.container_name])) + else: + exit(subprocess.call(['distrobox-enter', '-n', args.container_name, '-e', os.environ['BLEND_COMMAND']])) def create_container(): for container in args.pkg: @@ -381,7 +399,7 @@ def create_container(): def remove_container(): for container in args.pkg: info(f'removing container {container}') - subprocess.run(['podman', 'stop', container], stdout=subprocess.DEVNULL) + subprocess.run(['podman', 'stop', container], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) subprocess.run(['distrobox-rm', container, '--force'], stdout=subprocess.DEVNULL) def start_containers(): @@ -423,6 +441,8 @@ You can use any of these distros by passing the option {colors.bold}--distro=[NA You can even install a supported desktop environment in a blend container (run `blend install-de [DESKTOP ENVIRONMENT NAME]` to install your favorite desktop environment). +However, this feature is still somewhat experimental, and some apps might be buggy. + Here's a list of the supported desktop environments: {colors.bold}1.{colors.reset} gnome {colors.bold}2.{colors.reset} mate @@ -434,8 +454,10 @@ Here's a list of the supported desktop environments: {colors.bold}help{colors.reset} Show this help message and exit. {colors.bold}version{colors.reset} Show version information and exit. {colors.bold}enter{colors.reset} Enter the container shell. + {colors.bold}export{colors.reset} Export the desktop entry for an installed blend. + {colors.bold}unexport{colors.reset} Unexport the desktop entry for a blend. {colors.bold}install{colors.reset} Install packages inside a container. - {colors.bold}install-de{colors.reset} Install a desktop environment inside a container. + {colors.bold}install-de{colors.reset} Install a desktop environment inside a container. {colors.bold}(EXPERIMENTAL){colors.reset} {colors.bold}remove{colors.reset} Remove packages inside a managed container. {colors.bold}create-container{colors.reset} Create a container managed by blend. {colors.bold}remove-container{colors.reset} Remove a container managed by blend. @@ -471,6 +493,8 @@ command_map = { 'install': install_blend, 'start-containers': start_containers, 'sync': sync_blends, 'update': update_blends, + 'export': export_blend, + 'unexport': unexport_blend, 'search': search_blend, 'show': show_blend, 'help': 'help', @@ -488,19 +512,16 @@ if len(sys.argv) == 1: args = parser.parse_intermixed_args() +command = command_map[args.command] + distro_input = args.distro args.distro = default_distro if args.distro == None else args.distro[0] cn_input = args.container_name args.container_name = args.distro if args.container_name == None else args.container_name[0] -if args.container_name in distro_map and not args.distro == args.container_name and distro_input == None: - args.distro = args.container_name - info(f'assuming you meant to use {args.distro} as the distro') - -command = command_map[args.command] if command == 'help': parser.print_help() elif command == 'version': parser.parse_args(['--version']) else: - command() \ No newline at end of file + command() diff --git a/blend-pkgmngr-path.sh b/blend-pkgmngr-path.sh new file mode 100644 index 0000000..72901b4 --- /dev/null +++ b/blend-pkgmngr-path.sh @@ -0,0 +1,9 @@ +# shellcheck shell=sh + +# Expand $PATH to include the directory where blend's package manager shortcuts +# are located. +blend_pkgmanager_bin_path="/blend/pkgmanagers" +if [ -n "${PATH##*${blend_pkgmanager_bin_path}}" ] && [ -n "${PATH##*${blend_pkgmanager_bin_path}:*}" ]; then + export PATH="${blend_pkgmanager_bin_path}:${PATH}" +fi + diff --git a/completions/blend b/completions/blend index 70bbdee..4e7ed05 100755 --- a/completions/blend +++ b/completions/blend @@ -7,10 +7,11 @@ _completions() { if [[ "$COMP_CWORD" == 1 ]]; then SUGGESTIONS=('install' 'remove' 'update' 'show' 'search' 'enter' 'create-container' 'remove-container' \ - 'install-de' 'list-container' 'start-containers' 'sync' 'help' 'version' ) + 'install-de' 'list-containers' 'start-containers' 'sync' 'help' 'version' 'export' 'unexport', + '--container-name=' '--distro=' '--noconfirm' '--version') fi COMPREPLY=($(compgen -W "${SUGGESTIONS[*]}" "${COMP_WORDS[$COMP_CWORD]}")) } -complete -F _completions -- blend \ No newline at end of file +complete -F _completions -- blend diff --git a/debian/blend.install b/debian/blend.install index 3888acc..4182e87 100644 --- a/debian/blend.install +++ b/debian/blend.install @@ -1,2 +1,4 @@ blend /usr/bin/ +pkgmanagers /blend/ +blend-pkgmngr-path.sh /etc/profile.d/ completions/blend /usr/share/bash-completion/completions/ \ No newline at end of file diff --git a/debian/changelog b/debian/changelog index e89874f..e418a57 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +blend (1.0.3) lunar; urgency=emergency + + * Sync with blend 1.0.3, which updates the completions + + -- Rudra Bali Thu, 19 Jan 2023 14:27:17 +0530 + +blend (1.0.2) lunar; urgency=emergency + + * Sync with blend 1.0.2, which fixes many critical bugs + + -- Rudra Bali Thu, 19 Jan 2023 00:00:01 +0530 + blend (1.0.1) lunar; urgency=medium * Sync with blend 1.0.1 diff --git a/pkgmanagers/apt b/pkgmanagers/apt new file mode 100755 index 0000000..b0f337f --- /dev/null +++ b/pkgmanagers/apt @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +BLEND_COMMAND="sudo apt $@" blend enter ubuntu-22.10 --distro ubuntu-22.10 +ret=$? + +echo +echo -e '\033[01m\033[36m>> i: \033[0m\033[01mapps installed this way will need to be exported manually using `blend export [pkg]`,\033[0m' +echo -e '\033[01m\033[36m>> i: \033[0m\033[01mor you can run them by opening the Ubuntu-22.10 terminal (or running `blend enter -cn ubuntu-22.10`)\033[0m' + +exit $ret \ No newline at end of file diff --git a/pkgmanagers/apt-get b/pkgmanagers/apt-get new file mode 100755 index 0000000..c2f01ec --- /dev/null +++ b/pkgmanagers/apt-get @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +BLEND_COMMAND="sudo apt-get $@" blend enter ubuntu-22.10 --distro ubuntu-22.10 +ret=$? + +echo +echo -e '\033[01m\033[36m>> i: \033[0m\033[01mapps installed this way will need to be exported manually using `blend export [pkg]`,\033[0m' +echo -e '\033[01m\033[36m>> i: \033[0m\033[01mor you can run them by opening the Ubuntu-22.10 terminal (or running `blend enter -cn ubuntu-22.10`)\033[0m' + +exit $ret \ No newline at end of file diff --git a/pkgmanagers/dnf b/pkgmanagers/dnf new file mode 100755 index 0000000..6767b5d --- /dev/null +++ b/pkgmanagers/dnf @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +BLEND_COMMAND="sudo dnf $@" blend enter fedora-rawhide --distro fedora-rawhide +ret=$? + +echo +echo -e '\033[01m\033[36m>> i: \033[0m\033[01mapps installed this way will need to be exported manually using `blend export [pkg]`,\033[0m' +echo -e '\033[01m\033[36m>> i: \033[0m\033[01mor you can run them by opening the Fedora-rawhide terminal (or running `blend enter -cn fedora-rawhide`)\033[0m' + +exit $ret \ No newline at end of file diff --git a/pkgmanagers/pacman b/pkgmanagers/pacman new file mode 100755 index 0000000..fe89f03 --- /dev/null +++ b/pkgmanagers/pacman @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +BLEND_COMMAND="sudo pacman $@" blend enter arch +ret=$? + +echo +echo -e '\033[01m\033[36m>> i: \033[0m\033[01mapps installed this way will need to be exported manually using `blend export [pkg]`,\033[0m' +echo -e '\033[01m\033[36m>> i: \033[0m\033[01mor you can run them by opening the Arch terminal (or running `blend enter -cn arch`)\033[0m' + +exit $ret \ No newline at end of file diff --git a/pkgmanagers/yay b/pkgmanagers/yay new file mode 100755 index 0000000..0b4e061 --- /dev/null +++ b/pkgmanagers/yay @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +BLEND_COMMAND="yay $@" blend enter arch +ret=$? + +echo +echo -e '\033[01m\033[36m>> i: \033[0m\033[01mapps installed this way will need to be exported manually using `blend export [pkg]`,\033[0m' +echo -e '\033[01m\033[36m>> i: \033[0m\033[01mor you can run them by opening the Arch terminal (or running `blend enter -cn arch`)\033[0m' + +exit $ret \ No newline at end of file diff --git a/pkgmanagers/yum b/pkgmanagers/yum new file mode 100755 index 0000000..8a4fd06 --- /dev/null +++ b/pkgmanagers/yum @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +BLEND_COMMAND="sudo yum $@" blend enter fedora-rawhide --distro fedora-rawhide +ret=$? + +echo +echo -e '\033[01m\033[36m>> i: \033[0m\033[01mapps installed this way will need to be exported manually using `blend export [pkg]`,\033[0m' +echo -e '\033[01m\033[36m>> i: \033[0m\033[01mor you can run them by opening the Fedora-rawhide terminal (or running `blend enter -cn fedora-rawhide`)\033[0m' + +exit $ret \ No newline at end of file From 51a135b43eb1fffa4af4d348365d6d73fc876b51 Mon Sep 17 00:00:00 2001 From: Rudra Bali Date: Thu, 19 Jan 2023 14:54:53 +0530 Subject: [PATCH 005/121] update to 1.0.4 --- README.md | 2 +- blend | 4 ++-- debian/changelog | 6 ++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 359c6f4..ace8d08 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ This was originally developed by VanillaOS. Usage: blend [command] [options] [arguments] -Version: 1.0.3 +Version: 1.0.4 blend is a package manager for blendOS, which includes support for Arch, Ubuntu and Fedora packages. diff --git a/blend b/blend index 9b9cbe9..0c0d7c6 100755 --- a/blend +++ b/blend @@ -5,7 +5,7 @@ import shutil import argparse import subprocess -__version = '1.0.3' +__version = '1.0.4' ### Colors class colors: @@ -299,7 +299,7 @@ def copy_desktop_files(pkg): distrobox_run_container(f'CONTAINER_ID={args.container_name} distrobox-export --app {pkg} &>/dev/null') def del_desktop_files(pkg): - distrobox_run_container(f'CONTAINER_ID={args.container_name} distrobox-export --app {pkg} --delete &>/dev/null') + subprocess.call(['distrobox-enter', '-n', args.container_name, '-e', f'sh -c "CONTAINER_ID={args.container_name} distrobox-export --app {pkg} --delete &>/dev/null"']) def export_blend(): for pkg in args.pkg: diff --git a/debian/changelog b/debian/changelog index e418a57..c6747c5 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +blend (1.0.4) lunar; urgency=emergency + + * Sync with blend 1.0.4, which fixes a major bug relating to package removal + + -- Rudra Bali Thu, 19 Jan 2023 14:54:09 +0530 + blend (1.0.3) lunar; urgency=emergency * Sync with blend 1.0.3, which updates the completions From a8e78d935c419874bd04b187c565912eb899412d Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Thu, 19 Jan 2023 17:58:04 +0530 Subject: [PATCH 006/121] fix completions --- completions/blend | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/completions/blend b/completions/blend index 4e7ed05..7f88bfc 100755 --- a/completions/blend +++ b/completions/blend @@ -11,7 +11,7 @@ _completions() { '--container-name=' '--distro=' '--noconfirm' '--version') fi - COMPREPLY=($(compgen -W "${SUGGESTIONS[*]}" "${COMP_WORDS[$COMP_CWORD]}")) + COMPREPLY=($(compgen -W "${SUGGESTIONS[*]}" -- "${COMP_WORDS[$COMP_CWORD]}")) } complete -F _completions -- blend From 19112dcb3ec6371ac80ba249fe4195bb3b24c143 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Thu, 19 Jan 2023 18:07:07 +0530 Subject: [PATCH 007/121] initial commit --- .gitignore | 5 +++++ PKGBUILD | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 .gitignore create mode 100644 PKGBUILD diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6a9b95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +* + +!.gitignore +!PKGBUILD +!.SRCINFO diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..3d60c4a --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,22 @@ +# Maintainer: Rudra Saraswat + +pkgname=blend +pkgver=1.0.4 +pkgrel=1 +pkgdesc='A package manager for blendOS' +url='https://github.com/blend-os/blend' +source=("git+https://github.com/blend-os/blend.git") +sha256sums=('SKIP') +arch=('x86_64') +license=('GPL-3.0-or-later') +depends=('python3' 'distrobox' 'podman') + +package() { + cd blend + + mkdir -p "${pkgdir}"/{usr/{bin,share/bash-completion/completions},blend,etc/profile.d} + cp blend "${pkgdir}/usr/bin" + cp blend-pkgmngr-path.sh "${pkgdir}/etc/profile.d" + cp completions/blend "${pkgdir}/usr/share/bash-completion/completions" + cp -r pkgmanagers "${pkgdir}/blend" +} From e509574472c5c2624dc9e3be04bf9b79f9910c02 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Thu, 19 Jan 2023 23:37:31 +0530 Subject: [PATCH 008/121] add system-update --- blend | 8 ++++++++ completions/blend | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/blend b/blend index 0c0d7c6..28f7af5 100755 --- a/blend +++ b/blend @@ -382,6 +382,12 @@ def update_blends(): else: error(f'distribution {args.distro} is not supported') +def system_update(): + if args.noconfirm == True: + exit(subprocess.call(['sudo', '/usr/bin/pacman', '--noconfirm', '-Syu'])) + else: + exit(subprocess.call(['sudo', '/usr/bin/pacman', '-Syu'])) + def enter_container(): if not distrobox_check_container(args.container_name): distrobox_create_container() @@ -467,6 +473,7 @@ Here's a list of the supported desktop environments: {colors.bold}search{colors.reset} Search for packages in a managed container. {colors.bold}show{colors.reset} Show details about a package. {colors.bold}update{colors.reset} Update all the packages in a managed container. + {colors.bold}system-update{colors.reset} Update all the system packages. {colors.bold}{colors.fg.purple}options for commands{colors.reset}: {colors.bold}-cn CONTAINER NAME, --container-name CONTAINER NAME{colors.reset} @@ -493,6 +500,7 @@ command_map = { 'install': install_blend, 'start-containers': start_containers, 'sync': sync_blends, 'update': update_blends, + 'system-update': system_update, 'export': export_blend, 'unexport': unexport_blend, 'search': search_blend, diff --git a/completions/blend b/completions/blend index 7f88bfc..157621d 100755 --- a/completions/blend +++ b/completions/blend @@ -7,7 +7,7 @@ _completions() { if [[ "$COMP_CWORD" == 1 ]]; then SUGGESTIONS=('install' 'remove' 'update' 'show' 'search' 'enter' 'create-container' 'remove-container' \ - 'install-de' 'list-containers' 'start-containers' 'sync' 'help' 'version' 'export' 'unexport', + 'install-de' 'list-containers' 'start-containers' 'sync' 'help' 'version' 'export' 'unexport' 'system-update' \ '--container-name=' '--distro=' '--noconfirm' '--version') fi From 89452160ea1ea83f4a7203e268a60a8152b722c2 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 20 Jan 2023 11:37:31 +0530 Subject: [PATCH 009/121] remove debian packaging --- blend | 4 +-- blend-pkgmngr-path.sh | 9 ------ blend-profiled.sh | 73 +++++++++++++++++++++++++++++++++++++++++++ debian/blend.install | 4 --- debian/changelog | 29 ----------------- debian/compat | 1 - debian/control | 22 ------------- debian/copyright | 24 -------------- debian/rules | 4 --- debian/source/format | 1 - 10 files changed, 75 insertions(+), 96 deletions(-) delete mode 100644 blend-pkgmngr-path.sh create mode 100644 blend-profiled.sh delete mode 100644 debian/blend.install delete mode 100644 debian/changelog delete mode 100644 debian/compat delete mode 100644 debian/control delete mode 100644 debian/copyright delete mode 100755 debian/rules delete mode 100644 debian/source/format diff --git a/blend b/blend index 28f7af5..e6d6a92 100755 --- a/blend +++ b/blend @@ -384,9 +384,9 @@ def update_blends(): def system_update(): if args.noconfirm == True: - exit(subprocess.call(['sudo', '/usr/bin/pacman', '--noconfirm', '-Syu'])) + exit(subprocess.call(['sudo', 'almost', 'run', '/usr/bin/pacman', '--noconfirm', '-Syu'])) else: - exit(subprocess.call(['sudo', '/usr/bin/pacman', '-Syu'])) + exit(subprocess.call(['sudo', 'almost', 'run', '/usr/bin/pacman', '-Syu'])) def enter_container(): if not distrobox_check_container(args.container_name): diff --git a/blend-pkgmngr-path.sh b/blend-pkgmngr-path.sh deleted file mode 100644 index 72901b4..0000000 --- a/blend-pkgmngr-path.sh +++ /dev/null @@ -1,9 +0,0 @@ -# shellcheck shell=sh - -# Expand $PATH to include the directory where blend's package manager shortcuts -# are located. -blend_pkgmanager_bin_path="/blend/pkgmanagers" -if [ -n "${PATH##*${blend_pkgmanager_bin_path}}" ] && [ -n "${PATH##*${blend_pkgmanager_bin_path}:*}" ]; then - export PATH="${blend_pkgmanager_bin_path}:${PATH}" -fi - diff --git a/blend-profiled.sh b/blend-profiled.sh new file mode 100644 index 0000000..1d5bd5b --- /dev/null +++ b/blend-profiled.sh @@ -0,0 +1,73 @@ +# shellcheck shell=sh + +# Expand $PATH to include the directory where blend's package manager shortcuts +# are located. +blend_pkgmanager_bin_path="/blend/pkgmanagers" +if [ -n "${PATH##*${blend_pkgmanager_bin_path}}" ] && [ -n "${PATH##*${blend_pkgmanager_bin_path}:*}" ]; then + export PATH="${blend_pkgmanager_bin_path}:${PATH}" +fi + +# Start all the containers +blend start-containers &>/dev/null || : + +if [[ ! -f "${HOME}/.disable_blend_msg" ]]; then + shell_bold='\033[01m' + shell_color_purple='\033[35m' + shell_reset='\033[0m' + + echo -e "${shell_bold}Welcome to the ${shell_color_purple}blendOS${shell_reset}${shell_bold} shell!" + + echo -e "${shell_reset}note: if you don't want to see this message, you can create a file in your" + echo -e "home directory named .disable_blend_msg${shell_bold}" + echo + + echo -e 'Here are some useful commands:' + echo -e "${shell_reset}To install a package (from the Arch repos and the AUR) in an Arch container:${shell_bold}" + echo -e " blend install " + echo -e "${shell_reset}To remove a package (from the Arch repos and the AUR) in an Arch container:${shell_bold}" + echo -e " blend remove " + echo -e "${shell_reset}To install a package (from the Arch repos and the AUR) in a Fecora container:${shell_bold}" + echo -e " blend install -d fedora-rawhide" + echo -e "${shell_reset}To enter a Fedora container:${shell_bold}" + echo -e " blend enter -cn fedora-rawhide" + echo -e "${shell_reset}To update all the system packages:${shell_bold}" + echo -e " blend system-update${shell_reset} (do not use 'pacman -Syu', as it will only update the" + echo -e " packages in the Arch container)" + echo -e "${shell_reset}To list all the containers:${shell_bold}" + echo -e " blend list-containers" + + echo -e "Keep in mind that none of these commands should be run as root." + + echo + echo -e "${shell_reset}Most apps installed through blend will automatically appear in the applications" + echo -e "list. However, if they don't, you can always manually export them by running:" + echo -e " ${shell_bold}blend export [DESKTOP FILE WITHOUT EXTENSION]${shell_reset}" + echo + + echo -e "You can always specify a distribution (default is arch) by appending ${shell_bold}" + echo -e "--distro=[DISTRO]${shell_reset} to the end of a blend command." + echo -e "(for example: ${shell_bold}blend install hello --distro=ubuntu-22.10)${shell_reset}" + echo + echo -e "Here are the supported distributions:" + echo -e "${shell_bold}1.${shell_reset} arch (default)" + echo -e "${shell_bold}2.${shell_reset} fedora-rawhide" + echo -e "${shell_bold}3.${shell_reset} ubuntu-22.04" + echo -e "${shell_bold}4.${shell_reset} ubuntu-22.10" + echo -e "You can also specify a custom container name (default is the distro's name) by" + echo -e "appending ${shell_bold}--container-name=[CONTAINER]${shell_reset} to the end of a blend command." + + echo + echo -e "You can also use these packages managers directly:" + echo -e "${shell_bold}1.${shell_reset} pacman/yay (distro: arch)" + echo -e "${shell_bold}2.${shell_reset} apt/apt-get (distro: ubuntu-22.10)" + echo -e "${shell_bold}3.${shell_reset} dnf/yum (distro: fedora-rawhide)" + echo -e "However, you'll need to manually export the desktop files" + echo -e "for packages installed this way, by running:" + echo -e " ${shell_bold}blend export [DESKTOP FILE WITHOUT EXTENSION] --distro=[DISTRO]${shell_reset}" + + echo + echo -e "For more information about ${shell_color_purple}blend${shell_reset}${shell_bold}, run:" + echo -e " blend help" + + echo -e "${shell_reset}" +fi diff --git a/debian/blend.install b/debian/blend.install deleted file mode 100644 index 4182e87..0000000 --- a/debian/blend.install +++ /dev/null @@ -1,4 +0,0 @@ -blend /usr/bin/ -pkgmanagers /blend/ -blend-pkgmngr-path.sh /etc/profile.d/ -completions/blend /usr/share/bash-completion/completions/ \ No newline at end of file diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index c6747c5..0000000 --- a/debian/changelog +++ /dev/null @@ -1,29 +0,0 @@ -blend (1.0.4) lunar; urgency=emergency - - * Sync with blend 1.0.4, which fixes a major bug relating to package removal - - -- Rudra Bali Thu, 19 Jan 2023 14:54:09 +0530 - -blend (1.0.3) lunar; urgency=emergency - - * Sync with blend 1.0.3, which updates the completions - - -- Rudra Bali Thu, 19 Jan 2023 14:27:17 +0530 - -blend (1.0.2) lunar; urgency=emergency - - * Sync with blend 1.0.2, which fixes many critical bugs - - -- Rudra Bali Thu, 19 Jan 2023 00:00:01 +0530 - -blend (1.0.1) lunar; urgency=medium - - * Sync with blend 1.0.1 - - -- Rudra Bali Wed, 18 Jan 2023 23:14:32 +0530 - -blend (1.0.0) lunar; urgency=medium - - * First Release - - -- Rudra Saraswat Wed, 18 Jan 2023 21:58:53 +0200 diff --git a/debian/compat b/debian/compat deleted file mode 100644 index 9d60796..0000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -11 \ No newline at end of file diff --git a/debian/control b/debian/control deleted file mode 100644 index 30b7544..0000000 --- a/debian/control +++ /dev/null @@ -1,22 +0,0 @@ -Source: blend -Section: devel -Priority: optional -Maintainer: Rudra Saraswat -Build-Depends: debhelper (>= 9), -Standards-Version: 3.9.6 -Homepage: https://github.com/blend-os/blend -Vcs-Browser: https://github.com/blend-os/blend -Vcs-Git: git://github.com/blend-os/blend.git - -Package: blend -Architecture: all -Depends: ${shlibs:Depends}, - ${misc:Depends}, - distrobox, - podman, - python3 -Breaks: mrtrix3 -Built-Using: ${misc:Built-Using} -Description: Package manager for blendOS. - This package contains the package manger for - blendOS. diff --git a/debian/copyright b/debian/copyright deleted file mode 100644 index f81f27e..0000000 --- a/debian/copyright +++ /dev/null @@ -1,24 +0,0 @@ -Format: http://dep.debian.net/deps/dep5 -Upstream-Name: Blend OS First Setup -Source: https://github.com/blend-os/ - -Files: * -Copyright: 2023 blendOS contributors - 2022 Vanilla-OS contributors -License: GPL-3.0 - -License: GPL-3.0 - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License. - . - This package is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - . - You should have received a copy of the GNU General Public License - along with this program. If not, see . - . - On Debian systems, the complete text of the GNU General - Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". \ No newline at end of file diff --git a/debian/rules b/debian/rules deleted file mode 100755 index 2d33f6a..0000000 --- a/debian/rules +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/make -f - -%: - dh $@ diff --git a/debian/source/format b/debian/source/format deleted file mode 100644 index 9f67427..0000000 --- a/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (native) \ No newline at end of file From d7763f02571e6c594c571d2e8b03725f33a3b8ab Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 20 Jan 2023 11:46:19 +0530 Subject: [PATCH 010/121] update system-update --- blend | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/blend b/blend index e6d6a92..716babb 100755 --- a/blend +++ b/blend @@ -384,9 +384,11 @@ def update_blends(): def system_update(): if args.noconfirm == True: - exit(subprocess.call(['sudo', 'almost', 'run', '/usr/bin/pacman', '--noconfirm', '-Syu'])) + ret = subprocess.call(['sudo', 'almost', 'run', '/usr/bin/pacman', '--noconfirm', '-Syu']) else: - exit(subprocess.call(['sudo', 'almost', 'run', '/usr/bin/pacman', '-Syu'])) + ret = subprocess.call(['sudo', 'almost', 'run', '/usr/bin/pacman', '-Syu']) + subprocess.call(['sudo', 'almost', 'enter', 'ro']) + exit(ret) def enter_container(): if not distrobox_check_container(args.container_name): From 5af01fbf2849121d7c36a2208f1bb7dbdcacf476 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 20 Jan 2023 11:49:37 +0530 Subject: [PATCH 011/121] add try-except to system-update for locking system --- blend | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/blend b/blend index 716babb..87fccaf 100755 --- a/blend +++ b/blend @@ -383,12 +383,16 @@ def update_blends(): error(f'distribution {args.distro} is not supported') def system_update(): - if args.noconfirm == True: - ret = subprocess.call(['sudo', 'almost', 'run', '/usr/bin/pacman', '--noconfirm', '-Syu']) - else: - ret = subprocess.call(['sudo', 'almost', 'run', '/usr/bin/pacman', '-Syu']) - subprocess.call(['sudo', 'almost', 'enter', 'ro']) - exit(ret) + try: + if args.noconfirm == True: + ret = subprocess.call(['sudo', 'almost', 'run', '/usr/bin/pacman', '--noconfirm', '-Syu']) + else: + ret = subprocess.call(['sudo', 'almost', 'run', '/usr/bin/pacman', '-Syu']) + subprocess.call(['sudo', 'almost', 'enter', 'ro']) + exit(ret) + except: + subprocess.call(['sudo', 'almost', 'enter', 'ro']) + exit(1) def enter_container(): if not distrobox_check_container(args.container_name): From 6444b109f6954e35235fb5a7392b31abc9b75bb5 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 20 Jan 2023 12:29:58 +0530 Subject: [PATCH 012/121] update pkgbuild --- PKGBUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PKGBUILD b/PKGBUILD index 3d60c4a..cf61804 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -16,7 +16,7 @@ package() { mkdir -p "${pkgdir}"/{usr/{bin,share/bash-completion/completions},blend,etc/profile.d} cp blend "${pkgdir}/usr/bin" - cp blend-pkgmngr-path.sh "${pkgdir}/etc/profile.d" + cp blend-profiled.sh "${pkgdir}/etc/profile.d" cp completions/blend "${pkgdir}/usr/share/bash-completion/completions" cp -r pkgmanagers "${pkgdir}/blend" } From 9b1b0d89b4a84aa78fe68d0514537c95a3e9f14f Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 20 Jan 2023 22:57:29 +0530 Subject: [PATCH 013/121] remove blend-profiled.sh --- PKGBUILD | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index cf61804..072a0b2 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -14,9 +14,8 @@ depends=('python3' 'distrobox' 'podman') package() { cd blend - mkdir -p "${pkgdir}"/{usr/{bin,share/bash-completion/completions},blend,etc/profile.d} + mkdir -p "${pkgdir}"/{usr/{bin,share/bash-completion/completions},blend} cp blend "${pkgdir}/usr/bin" - cp blend-profiled.sh "${pkgdir}/etc/profile.d" cp completions/blend "${pkgdir}/usr/share/bash-completion/completions" cp -r pkgmanagers "${pkgdir}/blend" } From 608a72e0d51fecd159a52659c2da4651d18c079e Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sun, 22 Jan 2023 20:27:57 +0530 Subject: [PATCH 014/121] Update README.md --- README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index ace8d08..da3d8e2 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,7 @@

A package manager for blendOS

-# almost -On-demand immutability for blendOS. - -This was originally developed by VanillaOS. - -> **Note**: This is a work in progress. It is not ready for production use. +**rs2009 here. I've used a README from a Vanilla OS repository (almost) as a template, and accidentally included a paragraph from it. I've written this package manager from scratch (none of the other files in this repository have any code from any other repository on GitHub, other than anything developed me).** ### Help ``` From 23c20c0b7191a0d2d79355c6b74543062bcf16c5 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sun, 22 Jan 2023 20:52:06 +0530 Subject: [PATCH 015/121] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index da3d8e2..7cc91f3 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

A package manager for blendOS

-**rs2009 here. I've used a README from a Vanilla OS repository (almost) as a template, and accidentally included a paragraph from it. I've written this package manager from scratch (none of the other files in this repository have any code from any other repository on GitHub, other than anything developed me).** +**rs2009 here. I've used a README from a Vanilla OS repository (almost) as a template, and accidentally included a paragraph from it. I've written this package manager from scratch (none of the files aside from the README in this repository have any code from any other repository on GitHub, other than anything developed by me).** ### Help ``` From d2415704742b1aa0e8b9ccc10e08df14931081c1 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sun, 22 Jan 2023 20:52:57 +0530 Subject: [PATCH 016/121] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 7cc91f3..8f1b2f5 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@

blend

A package manager for blendOS

+

rs2009 here. I've used a README from a Vanilla OS repository (almost) as a template, and accidentally included a paragraph from it. I've written this package manager from scratch (none of the files aside from the README in this repository have any code from any other repository on GitHub, other than anything developed by me).

- -**rs2009 here. I've used a README from a Vanilla OS repository (almost) as a template, and accidentally included a paragraph from it. I've written this package manager from scratch (none of the files aside from the README in this repository have any code from any other repository on GitHub, other than anything developed by me).** - ### Help ``` Usage: From 37f20cae930d029138d2e5716e72fb9cd824b6bf Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sun, 22 Jan 2023 20:53:09 +0530 Subject: [PATCH 017/121] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8f1b2f5..f644e1b 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@

A package manager for blendOS

rs2009 here. I've used a README from a Vanilla OS repository (almost) as a template, and accidentally included a paragraph from it. I've written this package manager from scratch (none of the files aside from the README in this repository have any code from any other repository on GitHub, other than anything developed by me).

+ ### Help ``` Usage: From 6b5f7f987ad03e5c47e9f5f5668769dadbe975db Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Thu, 26 Jan 2023 12:35:10 +0530 Subject: [PATCH 018/121] switch to nearly --- blend | 12 +++++++----- pkgmanagers/apt | 6 +----- pkgmanagers/apt-get | 6 +----- pkgmanagers/dnf | 6 +----- pkgmanagers/pacman | 10 +++++----- pkgmanagers/yay | 6 +----- pkgmanagers/yum | 6 +----- 7 files changed, 17 insertions(+), 35 deletions(-) diff --git a/blend b/blend index 87fccaf..6bb0359 100755 --- a/blend +++ b/blend @@ -385,13 +385,15 @@ def update_blends(): def system_update(): try: if args.noconfirm == True: - ret = subprocess.call(['sudo', 'almost', 'run', '/usr/bin/pacman', '--noconfirm', '-Syu']) + ret = subprocess.call(['sudo', 'nearly', 'run', '/usr/bin/pacman --noconfirm -Syu']) else: - ret = subprocess.call(['sudo', 'almost', 'run', '/usr/bin/pacman', '-Syu']) - subprocess.call(['sudo', 'almost', 'enter', 'ro']) - exit(ret) + ret = subprocess.call(['sudo', 'nearly', 'run', '/usr/bin/pacman -Syu']) except: - subprocess.call(['sudo', 'almost', 'enter', 'ro']) + try: + subprocess.call(['sudo', 'nearly', 'enter', 'ro']) + except KeyboardInterrupt: + error('looks like you interrupted blend. your system is currently in read-write mode') + info('run `nearly enter ro\' to enable immutability again') exit(1) def enter_container(): diff --git a/pkgmanagers/apt b/pkgmanagers/apt index b0f337f..1af4b30 100755 --- a/pkgmanagers/apt +++ b/pkgmanagers/apt @@ -3,8 +3,4 @@ BLEND_COMMAND="sudo apt $@" blend enter ubuntu-22.10 --distro ubuntu-22.10 ret=$? -echo -echo -e '\033[01m\033[36m>> i: \033[0m\033[01mapps installed this way will need to be exported manually using `blend export [pkg]`,\033[0m' -echo -e '\033[01m\033[36m>> i: \033[0m\033[01mor you can run them by opening the Ubuntu-22.10 terminal (or running `blend enter -cn ubuntu-22.10`)\033[0m' - -exit $ret \ No newline at end of file +exit $ret diff --git a/pkgmanagers/apt-get b/pkgmanagers/apt-get index c2f01ec..5f757f6 100755 --- a/pkgmanagers/apt-get +++ b/pkgmanagers/apt-get @@ -3,8 +3,4 @@ BLEND_COMMAND="sudo apt-get $@" blend enter ubuntu-22.10 --distro ubuntu-22.10 ret=$? -echo -echo -e '\033[01m\033[36m>> i: \033[0m\033[01mapps installed this way will need to be exported manually using `blend export [pkg]`,\033[0m' -echo -e '\033[01m\033[36m>> i: \033[0m\033[01mor you can run them by opening the Ubuntu-22.10 terminal (or running `blend enter -cn ubuntu-22.10`)\033[0m' - -exit $ret \ No newline at end of file +exit $ret diff --git a/pkgmanagers/dnf b/pkgmanagers/dnf index 6767b5d..4e5f766 100755 --- a/pkgmanagers/dnf +++ b/pkgmanagers/dnf @@ -3,8 +3,4 @@ BLEND_COMMAND="sudo dnf $@" blend enter fedora-rawhide --distro fedora-rawhide ret=$? -echo -echo -e '\033[01m\033[36m>> i: \033[0m\033[01mapps installed this way will need to be exported manually using `blend export [pkg]`,\033[0m' -echo -e '\033[01m\033[36m>> i: \033[0m\033[01mor you can run them by opening the Fedora-rawhide terminal (or running `blend enter -cn fedora-rawhide`)\033[0m' - -exit $ret \ No newline at end of file +exit $ret diff --git a/pkgmanagers/pacman b/pkgmanagers/pacman index fe89f03..610be8d 100755 --- a/pkgmanagers/pacman +++ b/pkgmanagers/pacman @@ -1,10 +1,10 @@ #!/usr/bin/env bash +[[ -z "$SUDO_USER" ]] || { + [[ "$SUDO_USER" == root ]] || { SUDO_USER= sudo -u "$SUDO_USER" "$0" "$@"; exit $?; } +} + BLEND_COMMAND="sudo pacman $@" blend enter arch ret=$? -echo -echo -e '\033[01m\033[36m>> i: \033[0m\033[01mapps installed this way will need to be exported manually using `blend export [pkg]`,\033[0m' -echo -e '\033[01m\033[36m>> i: \033[0m\033[01mor you can run them by opening the Arch terminal (or running `blend enter -cn arch`)\033[0m' - -exit $ret \ No newline at end of file +exit $ret diff --git a/pkgmanagers/yay b/pkgmanagers/yay index 0b4e061..d53807d 100755 --- a/pkgmanagers/yay +++ b/pkgmanagers/yay @@ -3,8 +3,4 @@ BLEND_COMMAND="yay $@" blend enter arch ret=$? -echo -echo -e '\033[01m\033[36m>> i: \033[0m\033[01mapps installed this way will need to be exported manually using `blend export [pkg]`,\033[0m' -echo -e '\033[01m\033[36m>> i: \033[0m\033[01mor you can run them by opening the Arch terminal (or running `blend enter -cn arch`)\033[0m' - -exit $ret \ No newline at end of file +exit $ret diff --git a/pkgmanagers/yum b/pkgmanagers/yum index 8a4fd06..120463c 100755 --- a/pkgmanagers/yum +++ b/pkgmanagers/yum @@ -3,8 +3,4 @@ BLEND_COMMAND="sudo yum $@" blend enter fedora-rawhide --distro fedora-rawhide ret=$? -echo -echo -e '\033[01m\033[36m>> i: \033[0m\033[01mapps installed this way will need to be exported manually using `blend export [pkg]`,\033[0m' -echo -e '\033[01m\033[36m>> i: \033[0m\033[01mor you can run them by opening the Fedora-rawhide terminal (or running `blend enter -cn fedora-rawhide`)\033[0m' - -exit $ret \ No newline at end of file +exit $ret From 30ff6836561289108e8638d94e2b0fae63b2babd Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Thu, 26 Jan 2023 12:38:31 +0530 Subject: [PATCH 019/121] remove message from README --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index f644e1b..3095ae3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@

blend

A package manager for blendOS

-

rs2009 here. I've used a README from a Vanilla OS repository (almost) as a template, and accidentally included a paragraph from it. I've written this package manager from scratch (none of the files aside from the README in this repository have any code from any other repository on GitHub, other than anything developed by me).

### Help From 5c22f497b345d937015c3f039b804be5386e7420 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Thu, 26 Jan 2023 15:14:33 +0530 Subject: [PATCH 020/121] add `nearly enter rw` before installing DE --- blend | 204 ++++++++++++++++++++++++++++++---------------------------- 1 file changed, 107 insertions(+), 97 deletions(-) diff --git a/blend b/blend index 6bb0359..370596d 100755 --- a/blend +++ b/blend @@ -197,103 +197,113 @@ def install_de(): info(f'installing {name} on {args.container_name} (using {args.distro})') print() info('ignore any errors after this') - distrobox_run_container('sudo umount /run/systemd/system') - distrobox_run_container('sudo rmdir /run/systemd/system') - distrobox_run_container('sudo ln -s /run/host/run/systemd/system /run/systemd &> /dev/null') - distrobox_run_container('sudo ln -s /run/host/run/dbus/system_bus_socket /run/dbus/ &> /dev/null') - distrobox_run_container('sudo mkdir -p /usr/local/bin') - distrobox_run_container('echo "#!/usr/bin/env bash" | sudo tee /usr/local/bin/disable_dbusactivatable_blend') - distrobox_run_container('echo "if [[ \$DISABLE_REPEAT -ne 1 ]]; then" | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend') - distrobox_run_container('''echo 'while true; do for dir in ${XDG_DATA_DIRS//:/ }; do sudo find $dir/applications -type f -exec sed -i -e '"'s/DBusActivatable=.*//g'"' {} '"';'"'; done; sleep 5; done' | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend''') - distrobox_run_container('echo "else" | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend') - distrobox_run_container('''echo 'for dir in ${XDG_DATA_DIRS//:/ }; do sudo find $dir/applications -type f -exec sed -i -e '"'s/DBusActivatable=.*//g'"' {} '"';'"'; done' | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend''') - distrobox_run_container('echo "fi" | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend') - distrobox_run_container('sudo chmod 755 /usr/local/bin/disable_dbusactivatable_blend') - distrobox_run_container('sudo mkdir -p /etc/xdg/autostart') - distrobox_run_container('echo "[Desktop Entry]" | sudo tee /usr/share/applications/disable_dbusactivatable_blend.desktop') - distrobox_run_container('echo "Version=1.0" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') - distrobox_run_container('echo "Name=Remove DBusActivatable (blend)" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') - distrobox_run_container('echo "Comment=Remove DBusActivatable in all desktop files" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') - distrobox_run_container('echo "Exec=disable_dbusactivatable_blend" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') - distrobox_run_container('echo "Type=Application" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') - distrobox_run_container('sudo chmod 755 /usr/share/applications/disable_dbusactivatable_blend.desktop') - if name == 'mate': - if args.distro == 'fedora-rawhide': - if args.noconfirm == True: - distrobox_run_container(f'sudo dnf --allowerasing -y groupinstall MATE') - else: - distrobox_run_container(f'sudo dnf --allowerasing groupinstall MATE') - elif args.distro == 'arch': - if args.noconfirm == True: - distrobox_run_container(f'sudo pacman --noconfirm -S mate mate-extra') - else: - distrobox_run_container(f'sudo pacman -S mate mate-extra') - elif args.distro.startswith('ubuntu-'): - distrobox_run_container(f'sudo apt-get update') - if args.noconfirm == True: - distrobox_run_container(f'sudo apt-get install -y ubuntu-mate-desktop') - else: - distrobox_run_container(f'sudo apt-get install ubuntu-mate-desktop') - subprocess.run('sudo mkdir -p /usr/local/bin', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "#!/usr/bin/env sh" | sudo tee /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "export GTK_MODULES=" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "export GTK3_MODULES=" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "export LD_PRELOAD=" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "chown -f -R $USER:$USER /tmp/.X11-unix" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "distrobox-enter {args.container_name} -- mate-session" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'sudo chmod 755 /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "[Desktop Entry]" | sudo tee /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Version=1.0" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Name=MATE ({args.container_name})" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Comment=Use this session to run MATE as your desktop environment" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Exec={args.container_name}-mate-blend" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Type=Application" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'sudo chmod 755 /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - elif name == 'gnome': - if args.distro == 'fedora-rawhide': - if args.noconfirm == True: - distrobox_run_container(f'sudo dnf --allowerasing -y groupinstall GNOME') - distrobox_run_container(f'sudo dnf --allowerasing -y remove gnome-terminal') - distrobox_run_container(f'sudo dnf --allowerasing -y install gnome-console') - else: - distrobox_run_container(f'sudo dnf --allowerasing groupinstall GNOME') - distrobox_run_container(f'sudo dnf --allowerasing -y remove gnome-terminal') - distrobox_run_container(f'sudo dnf --allowerasing -y install gnome-console') - elif args.distro == 'arch': - if args.noconfirm == True: - distrobox_run_container(f'sudo pacman --noconfirm -S gnome gnome-console') - distrobox_run_container(f'sudo pacman --noconfirm -Rcns gnome-terminal') - else: - distrobox_run_container(f'sudo pacman -S gnome gnome-console') - distrobox_run_container(f'sudo pacman --noconfirm -Rcns gnome-terminal') - elif args.distro.startswith('ubuntu-'): - distrobox_run_container(f'sudo apt-get update') - if args.noconfirm == True: - distrobox_run_container(f'sudo apt-get install -y ubuntu-desktop gnome-console') - distrobox_run_container(f'sudo apt-get purge -y gnome-terminal') - distrobox_run_container(f'sudo apt-get purge -y --auto-remove') - else: - distrobox_run_container(f'sudo apt-get install ubuntu-desktop gnome-console') - distrobox_run_container(f'sudo apt-get purge -y gnome-terminal') - distrobox_run_container(f'sudo apt-get purge -y --auto-remove') - subprocess.run(f'echo "[Desktop Entry]" | sudo tee /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Name=GNOME on Wayland ({args.container_name})" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Comment=This session logs you into GNOME" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Exec=distrobox-enter -n {args.container_name} -- gnome-session --builtin" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Type=Application" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "DesktopNames=GNOME" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "X-GDM-SessionRegisters=true" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "[Desktop Entry]" | sudo tee /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Name=GNOME on Xorg ({args.container_name})" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Comment=This session logs you into GNOME" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Exec=distrobox-enter -n {args.container_name} -- gnome-session --builtin" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Type=Application" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "DesktopNames=GNOME" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "X-GDM-SessionRegisters=true" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'sudo chmod 755 /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'sudo chmod 755 /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - distrobox_run_container('sudo mkdir -p /usr/share/applications /usr/local/share/applications') - distrobox_run_container('''echo 'for dir in /usr/share/applications /usr/local/share/applications; do sudo find $dir -type f -exec sed -i -e '"'s/DBusActivatable=.*//g'"' {} '"';'"'; done' | sudo bash''') + try: + subprocess.run('sudo nearly enter rw', shell=True) + distrobox_run_container('sudo umount /run/systemd/system') + distrobox_run_container('sudo rmdir /run/systemd/system') + distrobox_run_container('sudo ln -s /run/host/run/systemd/system /run/systemd &> /dev/null') + distrobox_run_container('sudo ln -s /run/host/run/dbus/system_bus_socket /run/dbus/ &> /dev/null') + distrobox_run_container('sudo mkdir -p /usr/local/bin') + distrobox_run_container('echo "#!/usr/bin/env bash" | sudo tee /usr/local/bin/disable_dbusactivatable_blend') + distrobox_run_container('echo "if [[ \$DISABLE_REPEAT -ne 1 ]]; then" | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend') + distrobox_run_container('''echo 'while true; do for dir in ${XDG_DATA_DIRS//:/ }; do sudo find $dir/applications -type f -exec sed -i -e '"'s/DBusActivatable=.*//g'"' {} '"';'"'; done; sleep 5; done' | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend''') + distrobox_run_container('echo "else" | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend') + distrobox_run_container('''echo 'for dir in ${XDG_DATA_DIRS//:/ }; do sudo find $dir/applications -type f -exec sed -i -e '"'s/DBusActivatable=.*//g'"' {} '"';'"'; done' | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend''') + distrobox_run_container('echo "fi" | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend') + distrobox_run_container('sudo chmod 755 /usr/local/bin/disable_dbusactivatable_blend') + distrobox_run_container('sudo mkdir -p /etc/xdg/autostart') + distrobox_run_container('echo "[Desktop Entry]" | sudo tee /usr/share/applications/disable_dbusactivatable_blend.desktop') + distrobox_run_container('echo "Version=1.0" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') + distrobox_run_container('echo "Name=Remove DBusActivatable (blend)" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') + distrobox_run_container('echo "Comment=Remove DBusActivatable in all desktop files" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') + distrobox_run_container('echo "Exec=disable_dbusactivatable_blend" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') + distrobox_run_container('echo "Type=Application" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') + distrobox_run_container('sudo chmod 755 /usr/share/applications/disable_dbusactivatable_blend.desktop') + if name == 'mate': + if args.distro == 'fedora-rawhide': + if args.noconfirm == True: + distrobox_run_container(f'sudo dnf --allowerasing -y groupinstall MATE') + else: + distrobox_run_container(f'sudo dnf --allowerasing groupinstall MATE') + elif args.distro == 'arch': + if args.noconfirm == True: + distrobox_run_container(f'sudo pacman --noconfirm -S mate mate-extra') + else: + distrobox_run_container(f'sudo pacman -S mate mate-extra') + elif args.distro.startswith('ubuntu-'): + distrobox_run_container(f'sudo apt-get update') + if args.noconfirm == True: + distrobox_run_container(f'sudo apt-get install -y ubuntu-mate-desktop') + else: + distrobox_run_container(f'sudo apt-get install ubuntu-mate-desktop') + subprocess.run('sudo mkdir -p /usr/local/bin', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "#!/usr/bin/env sh" | sudo tee /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "export GTK_MODULES=" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "export GTK3_MODULES=" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "export LD_PRELOAD=" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "chown -f -R $USER:$USER /tmp/.X11-unix" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "distrobox-enter {args.container_name} -- mate-session" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'sudo chmod 755 /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "[Desktop Entry]" | sudo tee /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Version=1.0" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Name=MATE ({args.container_name})" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Comment=Use this session to run MATE as your desktop environment" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Exec={args.container_name}-mate-blend" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Type=Application" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'sudo chmod 755 /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + elif name == 'gnome': + if args.distro == 'fedora-rawhide': + if args.noconfirm == True: + distrobox_run_container(f'sudo dnf --allowerasing -y groupinstall GNOME') + distrobox_run_container(f'sudo dnf --allowerasing -y remove gnome-terminal') + distrobox_run_container(f'sudo dnf --allowerasing -y install gnome-console') + else: + distrobox_run_container(f'sudo dnf --allowerasing groupinstall GNOME') + distrobox_run_container(f'sudo dnf --allowerasing -y remove gnome-terminal') + distrobox_run_container(f'sudo dnf --allowerasing -y install gnome-console') + elif args.distro == 'arch': + if args.noconfirm == True: + distrobox_run_container(f'sudo pacman --noconfirm -S gnome gnome-console') + distrobox_run_container(f'sudo pacman --noconfirm -Rcns gnome-terminal') + else: + distrobox_run_container(f'sudo pacman -S gnome gnome-console') + distrobox_run_container(f'sudo pacman --noconfirm -Rcns gnome-terminal') + elif args.distro.startswith('ubuntu-'): + distrobox_run_container(f'sudo apt-get update') + if args.noconfirm == True: + distrobox_run_container(f'sudo apt-get install -y ubuntu-desktop gnome-console') + distrobox_run_container(f'sudo apt-get purge -y gnome-terminal') + distrobox_run_container(f'sudo apt-get purge -y --auto-remove') + else: + distrobox_run_container(f'sudo apt-get install ubuntu-desktop gnome-console') + distrobox_run_container(f'sudo apt-get purge -y gnome-terminal') + distrobox_run_container(f'sudo apt-get purge -y --auto-remove') + subprocess.run(f'echo "[Desktop Entry]" | sudo tee /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Name=GNOME on Wayland ({args.container_name})" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Comment=This session logs you into GNOME" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Exec=distrobox-enter -n {args.container_name} -- gnome-session --builtin" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Type=Application" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "DesktopNames=GNOME" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "X-GDM-SessionRegisters=true" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "[Desktop Entry]" | sudo tee /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Name=GNOME on Xorg ({args.container_name})" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Comment=This session logs you into GNOME" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Exec=distrobox-enter -n {args.container_name} -- gnome-session --builtin" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "Type=Application" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "DesktopNames=GNOME" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'echo "X-GDM-SessionRegisters=true" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'sudo chmod 755 /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + subprocess.run(f'sudo chmod 755 /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) + distrobox_run_container('sudo mkdir -p /usr/share/applications /usr/local/share/applications') + distrobox_run_container('''echo 'for dir in /usr/share/applications /usr/local/share/applications; do sudo find $dir -type f -exec sed -i -e '"'s/DBusActivatable=.*//g'"' {} '"';'"'; done' | sudo bash''') + subprocess.run('sudo nearly enter ro', shell=True) + except: + try: + subprocess.call(['sudo', 'nearly', 'enter', 'ro']) + except KeyboardInterrupt: + error('looks like you interrupted blend. your system is currently in read-write mode') + info('run `nearly enter ro\' to enable immutability again') + exit(1) def copy_desktop_files(pkg): distrobox_run_container(f'CONTAINER_ID={args.container_name} distrobox-export --app {pkg} &>/dev/null') From 3fde9f6c98f7fd2015f921c184be673775383620 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Thu, 26 Jan 2023 15:48:52 +0530 Subject: [PATCH 021/121] update behaviour of create-container --- blend | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blend b/blend index 370596d..0732f5c 100755 --- a/blend +++ b/blend @@ -417,6 +417,8 @@ def enter_container(): def create_container(): for container in args.pkg: args.container_name = container + if container in distro_map.keys() and distro_input == None: + arg.distro = container distrobox_create_container() subprocess.call(['distrobox-enter', '-nw', '-n', args.container_name, '--', 'echo -n']) From 6febeb785822d092e5ecdab187496d310ee50ed3 Mon Sep 17 00:00:00 2001 From: Robin Candau Date: Fri, 27 Jan 2023 14:46:26 +0100 Subject: [PATCH 022/121] Deleted the '(from the Arch repo [...])' bit and corrected the Fedora typo --- blend-profiled.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blend-profiled.sh b/blend-profiled.sh index 1d5bd5b..425fae8 100644 --- a/blend-profiled.sh +++ b/blend-profiled.sh @@ -22,11 +22,11 @@ if [[ ! -f "${HOME}/.disable_blend_msg" ]]; then echo echo -e 'Here are some useful commands:' - echo -e "${shell_reset}To install a package (from the Arch repos and the AUR) in an Arch container:${shell_bold}" + echo -e "${shell_reset}To install a package in an Arch container:${shell_bold}" echo -e " blend install " - echo -e "${shell_reset}To remove a package (from the Arch repos and the AUR) in an Arch container:${shell_bold}" + echo -e "${shell_reset}To remove a package in an Arch container:${shell_bold}" echo -e " blend remove " - echo -e "${shell_reset}To install a package (from the Arch repos and the AUR) in a Fecora container:${shell_bold}" + echo -e "${shell_reset}To install a package in a Fedora container:${shell_bold}" echo -e " blend install -d fedora-rawhide" echo -e "${shell_reset}To enter a Fedora container:${shell_bold}" echo -e " blend enter -cn fedora-rawhide" From 4e4bdd33d2de374cb01e46c51b2af4719aeca509 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 27 Jan 2023 20:24:49 +0530 Subject: [PATCH 023/121] fix a typo --- blend | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blend b/blend index 0732f5c..2bd9152 100755 --- a/blend +++ b/blend @@ -418,7 +418,7 @@ def create_container(): for container in args.pkg: args.container_name = container if container in distro_map.keys() and distro_input == None: - arg.distro = container + args.distro = container distrobox_create_container() subprocess.call(['distrobox-enter', '-nw', '-n', args.container_name, '--', 'echo -n']) From c14a4d074094fa6d109e977aeea83903119edc67 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 27 Jan 2023 23:34:30 +0530 Subject: [PATCH 024/121] update pkgrel --- PKGBUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PKGBUILD b/PKGBUILD index 072a0b2..226102f 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -2,7 +2,7 @@ pkgname=blend pkgver=1.0.4 -pkgrel=1 +pkgrel=2 pkgdesc='A package manager for blendOS' url='https://github.com/blend-os/blend' source=("git+https://github.com/blend-os/blend.git") From 37d4a6155e9d5dbaf765dfb2a5346e585b9ddb5f Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sat, 11 Feb 2023 17:33:29 +0530 Subject: [PATCH 025/121] release blend 2.0.0 --- .gitignore | 7 +- LICENSE.md | 675 ++++++++++++++++++ README.md | 65 +- blend | 466 ++++++------ blend-files | 210 ++++++ blend-files.service | 10 + blend-profiled.sh | 73 -- blend-settings/.gitignore | 4 + LICENSE => blend-settings/LICENSE | 8 +- blend-settings/build/icon.png | Bin 0 -> 16225 bytes blend-settings/main.js | 156 ++++ blend-settings/package.json | 28 + .../src/external/css/bootstrap.min.css | 7 + .../src/external/js/bootstrap.bundle.min.js | 7 + blend-settings/src/index.html | 64 ++ blend-settings/src/internal/css/common.css | 98 +++ blend-settings/src/internal/js/containers.js | 184 +++++ .../src/internal/js/generic_page.js | 15 + blend-settings/src/internal/js/overlay.js | 132 ++++ blend-settings/src/pages/containers.html | 49 ++ blend-settings/src/pages/overlay.html | 64 ++ blend-settings/src/pages/terminal.html | 110 +++ blend-settings/src/preload.js | 0 blend-system | 183 +++++ blend-system.service | 10 + blend.hook | 22 + blend.install | 18 + completions/blend | 17 - host-blend | 36 + init-blend | 257 +++++++ pkgmanagers/apt | 6 - pkgmanagers/apt-get | 6 - pkgmanagers/dnf | 6 - pkgmanagers/pacman | 10 - pkgmanagers/yay | 6 - pkgmanagers/yum | 6 - 36 files changed, 2557 insertions(+), 458 deletions(-) create mode 100644 LICENSE.md create mode 100755 blend-files create mode 100644 blend-files.service delete mode 100644 blend-profiled.sh create mode 100644 blend-settings/.gitignore rename LICENSE => blend-settings/LICENSE (99%) create mode 100644 blend-settings/build/icon.png create mode 100644 blend-settings/main.js create mode 100644 blend-settings/package.json create mode 100644 blend-settings/src/external/css/bootstrap.min.css create mode 100644 blend-settings/src/external/js/bootstrap.bundle.min.js create mode 100644 blend-settings/src/index.html create mode 100644 blend-settings/src/internal/css/common.css create mode 100644 blend-settings/src/internal/js/containers.js create mode 100644 blend-settings/src/internal/js/generic_page.js create mode 100644 blend-settings/src/internal/js/overlay.js create mode 100644 blend-settings/src/pages/containers.html create mode 100644 blend-settings/src/pages/overlay.html create mode 100644 blend-settings/src/pages/terminal.html create mode 100644 blend-settings/src/preload.js create mode 100755 blend-system create mode 100644 blend-system.service create mode 100644 blend.hook create mode 100644 blend.install delete mode 100755 completions/blend create mode 100755 host-blend create mode 100755 init-blend delete mode 100755 pkgmanagers/apt delete mode 100755 pkgmanagers/apt-get delete mode 100755 pkgmanagers/dnf delete mode 100755 pkgmanagers/pacman delete mode 100755 pkgmanagers/yay delete mode 100755 pkgmanagers/yum diff --git a/.gitignore b/.gitignore index 41a53bc..4fe686f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,2 @@ -/debian/*.debhelper -/debian/*.substvars -/debian/debhelper-build-stamp -/debian/blend -/debian/files +/blend-settings/node_modules +/blend-settings/package-lock.json diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..f97a89a --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,675 @@ +# GNU GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +## Preamble + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom +to share and change all versions of a program--to make sure it remains +free software for all its users. We, the Free Software Foundation, use +the GNU General Public License for most of our software; it applies +also to any other work released this way by its authors. You can apply +it to your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you +have certain responsibilities if you distribute copies of the +software, or if you modify it: responsibilities to respect the freedom +of others. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the +manufacturer can do so. This is fundamentally incompatible with the +aim of protecting users' freedom to change the software. The +systematic pattern of such abuse occurs in the area of products for +individuals to use, which is precisely where it is most unacceptable. +Therefore, we have designed this version of the GPL to prohibit the +practice for those products. If such problems arise substantially in +other domains, we stand ready to extend this provision to those +domains in future versions of the GPL, as needed to protect the +freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish +to avoid the special danger that patents applied to a free program +could make it effectively proprietary. To prevent this, the GPL +assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and +modification follow. + +### TERMS AND CONDITIONS + +#### 0. Definitions + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds +of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of +an exact copy. The resulting work is called a "modified version" of +the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user +through a computer network, with no transfer of a copy, is not +conveying. + +An interactive user interface displays "Appropriate Legal Notices" to +the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +#### 1. Source Code + +The "source code" for a work means the preferred form of the work for +making modifications to it. "Object code" means any non-source form of +a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can +regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same +work. + +#### 2. Basic Permissions + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, +without conditions so long as your license otherwise remains in force. +You may convey covered works to others for the sole purpose of having +them make modifications exclusively for you, or provide you with +facilities for running those works, provided that you comply with the +terms of this License in conveying all material for which you do not +control copyright. Those thus making or running the covered works for +you must do so exclusively on your behalf, under your direction and +control, on terms that prohibit them from making any copies of your +copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the +conditions stated below. Sublicensing is not allowed; section 10 makes +it unnecessary. + +#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such +circumvention is effected by exercising rights under this License with +respect to the covered work, and you disclaim any intention to limit +operation or modification of the work as a means of enforcing, against +the work's users, your or third parties' legal rights to forbid +circumvention of technological measures. + +#### 4. Conveying Verbatim Copies + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +#### 5. Conveying Modified Source Versions + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these +conditions: + +- a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. +- b) The work must carry prominent notices stating that it is + released under this License and any conditions added under + section 7. This requirement modifies the requirement in section 4 + to "keep intact all notices". +- c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. +- d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +#### 6. Conveying Non-Source Forms + +You may convey a covered work in object code form under the terms of +sections 4 and 5, provided that you also convey the machine-readable +Corresponding Source under the terms of this License, in one of these +ways: + +- a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. +- b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the Corresponding + Source from a network server at no charge. +- c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. +- d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. +- e) Convey the object code using peer-to-peer transmission, + provided you inform other peers where the object code and + Corresponding Source of the work are being offered to the general + public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, +family, or household purposes, or (2) anything designed or sold for +incorporation into a dwelling. In determining whether a product is a +consumer product, doubtful cases shall be resolved in favor of +coverage. For a particular product received by a particular user, +"normally used" refers to a typical or common use of that class of +product, regardless of the status of the particular user or of the way +in which the particular user actually uses, or expects or is expected +to use, the product. A product is a consumer product regardless of +whether the product has substantial commercial, industrial or +non-consumer uses, unless such uses represent the only significant +mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to +install and execute modified versions of a covered work in that User +Product from a modified version of its Corresponding Source. The +information must suffice to ensure that the continued functioning of +the modified object code is in no case prevented or interfered with +solely because modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or +updates for a work that has been modified or installed by the +recipient, or for the User Product in which it has been modified or +installed. Access to a network may be denied when the modification +itself materially and adversely affects the operation of the network +or violates the rules and protocols for communication across the +network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +#### 7. Additional Terms + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders +of that material) supplement the terms of this License with terms: + +- a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or +- b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or +- c) Prohibiting misrepresentation of the origin of that material, + or requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or +- d) Limiting the use for publicity purposes of names of licensors + or authors of the material; or +- e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or +- f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions + of it) with contractual assumptions of liability to the recipient, + for any liability that these contractual assumptions directly + impose on those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; the +above requirements apply either way. + +#### 8. Termination + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your license +from a particular copyright holder is reinstated (a) provisionally, +unless and until the copyright holder explicitly and finally +terminates your license, and (b) permanently, if the copyright holder +fails to notify you of the violation by some reasonable means prior to +60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +#### 9. Acceptance Not Required for Having Copies + +You are not required to accept this License in order to receive or run +a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +#### 10. Automatic Licensing of Downstream Recipients + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +#### 11. Patents + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned +or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within the +scope of its coverage, prohibits the exercise of, or is conditioned on +the non-exercise of one or more of the rights that are specifically +granted under this License. You may not convey a covered work if you +are a party to an arrangement with a third party that is in the +business of distributing software, under which you make payment to the +third party based on the extent of your activity of conveying the +work, and under which the third party grants, to any of the parties +who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by +you (or copies made from those copies), or (b) primarily for and in +connection with specific products or compilations that contain the +covered work, unless you entered into that arrangement, or that patent +license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +#### 12. No Surrender of Others' Freedom + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under +this License and any other pertinent obligations, then as a +consequence you may not convey it at all. For example, if you agree to +terms that obligate you to collect a royalty for further conveying +from those to whom you convey the Program, the only way you could +satisfy both those terms and this License would be to refrain entirely +from conveying the Program. + +#### 13. Use with the GNU Affero General Public License + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +#### 14. Revised Versions of this License + +The Free Software Foundation may publish revised and/or new versions +of the GNU General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in +detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies that a certain numbered version of the GNU General Public +License "or any later version" applies to it, you have the option of +following the terms and conditions either of that numbered version or +of any later version published by the Free Software Foundation. If the +Program does not specify a version number of the GNU General Public +License, you may choose any version ever published by the Free +Software Foundation. + +If the Program specifies that a proxy can decide which future versions +of the GNU General Public License can be used, that proxy's public +statement of acceptance of a version permanently authorizes you to +choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +#### 15. Disclaimer of Warranty + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT +WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + +#### 16. Limitation of Liability + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR +CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT +NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR +LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM +TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER +PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +#### 17. Interpretation of Sections 15 and 16 + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +### How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these +terms. + +To do so, attach the following notices to the program. It is safest to +attach them to the start of each source file to most effectively state +the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper +mail. + +If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands \`show w' and \`show c' should show the +appropriate parts of the General Public License. Of course, your +program's commands might be different; for a GUI interface, you would +use an "about box". + +You should also get your employer (if you work as a programmer) or +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. For more information on this, and how to apply and follow +the GNU GPL, see . + +The GNU General Public License does not permit incorporating your +program into proprietary programs. If your program is a subroutine +library, you may consider it more useful to permit linking proprietary +applications with the library. If this is what you want to do, use the +GNU Lesser General Public License instead of this License. But first, +please read . diff --git a/README.md b/README.md index 3095ae3..efe7c86 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,16 @@

blend

-

A package manager for blendOS

+

A tool to manage overlays, containers and multiple distributions

-### Help -``` -Usage: - blend [command] [options] [arguments] +This repository also contains **blend-settings**, a tool for configuring blend and overlays, as well as many other utilities. -Version: 1.0.4 +## Credits -blend is a package manager for blendOS, which includes support for Arch, Ubuntu and Fedora packages. +The `init-blend` file in this repository uses a few lines (two sections) uses from distrobox's init script. These lines have been marked and attributed appropriately, and are licensed under [the GPL-3.0 license](https://github.com/89luca89/distrobox/blob/main/COPYING.md). -default distro: arch (default container's name is the same as that of the default distro) +Aside from these lines, all the other code in this repository has been written by me (rs2009). `blend-settings` is based on [Modren](https://github.com/RudraSwat/modren), a software store I (rs2009) had written long ago, and is licensed under the same license as the rest of the code in this repository, [the GPL-3.0 license](https://github.com/blend-os/blend/blob/main/LICENSE). -Here's a list of the supported distros: -1. arch -2. fedora-rawhide -3. ubuntu-22.04 -4. ubuntu-22.10 -(debian support is coming soon) +## Usage -You can use any of these distros by passing the option --distro=[NAME OF THE DISTRO]. - -You can even install a supported desktop environment in a blend container (run `blend install-de [DESKTOP ENVIRONMENT NAME]` to install your favorite desktop environment). - -However, this feature is still somewhat experimental, and some apps might be buggy. - -Here's a list of the supported desktop environments: -1. gnome -2. mate -(support for many more DEs is coming soon) - -arch also supports AUR packages, for an extremely large app catalog. - -available commands: - help Show this help message and exit. - version Show version information and exit. - enter Enter the container shell. - export Export the desktop entry for an installed blend. - unexport Unexport the desktop entry for a blend. - install Install packages inside a container. - install-de Install a desktop environment inside a container. (EXPERIMENTAL) - remove Remove packages inside a managed container. - create-container Create a container managed by blend. - remove-container Remove a container managed by blend. - list-containers List all the containers managed by blend. - start-containers Start all the container managed by blend. - sync Sync list of available packages from repository. - search Search for packages in a managed container. - show Show details about a package. - update Update all the packages in a managed container. - -options for commands: - -cn CONTAINER NAME, --container-name CONTAINER NAME - set the container name (the default is the name of the distro) - -d DISTRO, --distro DISTRO - set the distro name (supported: arch fedora-rawhide ubuntu-22.04 ubuntu-22.10; default is arch) - -y, --noconfirm assume yes for all questions - -v, --version show version information and exit - -options: - -h, --help show this help message and exit - -Made with ❤ by Rudra Saraswat. -``` +It's recommended to use the `blend-settings` UI, instead of the `blend` CLI. \ No newline at end of file diff --git a/blend b/blend index 2bd9152..abb58f5 100755 --- a/blend +++ b/blend @@ -1,11 +1,30 @@ #!/usr/bin/env python3 +# Copyright (C) 2023 Rudra Saraswat +# +# This file is part of blend. +# +# blend is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# blend is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with blend. If not, see . -import os, sys + +import os, sys, getpass, time import shutil +import socket +import pexpect import argparse import subprocess -__version = '1.0.4' +__version = '2.0.0' ### Colors class colors: @@ -58,9 +77,9 @@ def error(err): distro_map = { 'arch': 'docker.io/library/archlinux', - 'fedora-rawhide': 'fedora:rawhide', - 'ubuntu-22.04': 'ubuntu:22.04', - 'ubuntu-22.10': 'ubuntu:22.10' + 'fedora-rawhide': 'docker.io/library/fedora:rawhide', + 'ubuntu-22.04': 'docker.io/library/ubuntu:22.04', + 'ubuntu-22.10': 'docker.io/library/ubuntu:22.10' } default_distro = 'arch' @@ -72,267 +91,191 @@ def get_distro(): error(f"{args.distro} isn't supported by blend.") exit() -def distrobox_list_containers(): +def list_containers(): _list = subprocess.run(['podman', 'ps', '-a', '--no-trunc', '--size', '--format', '{{.Names}}:{{.Mounts}}'], stdout=subprocess.PIPE).stdout.decode('utf-8').strip() if len(_list) == 0: info('No containers. Create one by installing a package (`blend install hello`), or manually create one (`blend create-container arch`).') else: info('List of containers:') - for container in _list.splitlines(keepends=False): - if 'distrobox' in container.split(':')[1]: - print(container.split(':')[0]) + for i, container in enumerate(_list.splitlines(keepends=False)): + if 'blend' in container.split(':')[1]: + print(f"{colors.bold}{i}.{colors.reset} {container.split(':')[0]}") return False -def distrobox_check_container(name): +def check_container(name): _list = subprocess.run(['podman', 'ps', '-a', '--no-trunc', '--size', '--format', '{{.Names}}:{{.Mounts}}'], stdout=subprocess.PIPE).stdout.decode('utf-8').strip() for container in _list.splitlines(keepends=False): - if 'distrobox' in container.split(':')[1] and name.strip() == container.split(':')[0]: + if 'blend' in container.split(':')[1] and name.strip() == container.split(':')[0]: return True return False -def distrobox_create_container(): +def check_container_status(name): + return host_get_output("podman inspect --type container " + name + " --format \"{{.State.Status}}\"") + +def core_start_container(name): + subprocess.call(['podman', 'start', name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + start_time = time.time() - 1000 # workaround + if check_container_status(name) != 'running': + print('') + error('the entry point failed to run; try again later') + info("here are the container's logs:") + subprocess.call(['podman', 'logs', '--since', str(start_time), name]) + exit(1) + + logproc = pexpect.spawn('podman', args=['logs', '-f', '--since', str(start_time), name], timeout=300) + logproc.logfile_read = sys.stdout.buffer + + logproc.expect('Completed container setup') + logproc.terminate() + +def core_create_container(): name = args.container_name distro = args.distro info(f'creating container {name}, using {distro}') - if subprocess.run(['distrobox-create', '-Y', '-n', name, '-i', get_distro()], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0: - if distrobox_check_container(name): + + podman_command = [] + + # Basic stuff + podman_command.extend(['podman', 'create', '--name', name]) + podman_command.extend(['--hostname', name + '.' + socket.gethostname()]) + podman_command.extend(['--privileged', '--ipc', 'host']) + podman_command.extend(['--network', 'host']) + podman_command.extend(['--security-opt', 'label=disable']) + podman_command.extend(['--user', 'root:root', '--pid', 'host']) + podman_command.extend(['--label', 'manager=blend']) # identify as blend container + + # Env variables + podman_command.extend(['--env', 'HOME=' + os.path.expanduser('~')]) + + # Volumes + podman_command.extend(['--volume', '/:/run/host:rslave']) + podman_command.extend(['--volume', '/tmp:/tmp:rslave']) + podman_command.extend(['--volume', f"{os.path.expanduser('~')}:{os.path.expanduser('~')}:rslave"]) + podman_command.extend(['--volume', f"/run/user/{os.geteuid()}:/run/user/{os.geteuid()}:rslave"]) + + # Volumes (config files) + podman_command.extend(['--volume', f"/etc/hosts:/etc/hosts:ro"]) + podman_command.extend(['--volume', f"/etc/localtime:/etc/localtime:ro"]) + podman_command.extend(['--volume', f"/etc/resolv.conf:/etc/resolv.conf:ro"]) + + # Volumes (files and tools) + podman_command.extend(['--volume', '/usr/bin/init-blend:/usr/bin/init-blend:ro', + '--entrypoint', '/usr/bin/init-blend']) # our entrypoint + podman_command.extend(['--volume', '/usr/bin/host-blend:/usr/bin/host-blend:ro']) # and the tool to run commands on the host + podman_command.extend(['--volume', '/var/log/journal']) + + podman_command.extend(['--mount', 'type=devpts,destination=/dev/pts', + '--userns', 'keep-id', + '--annotation', 'run.oci.keep_original_groups=1']) + + podman_command.extend([get_distro()]) + + # User (for init-blend) + podman_command.extend(['--uid', str(os.geteuid())]) + podman_command.extend(['--group', str(os.getgid())]) + podman_command.extend(['--username', getpass.getuser()]) + podman_command.extend(['--home', os.path.expanduser('~')]) + + ret = subprocess.run(podman_command).returncode + if ret != 0: + if check_container(name): error(f'container {name} already exists') exit(1) error(f'failed to create container {name}') exit(1) - subprocess.call(['distrobox-enter', '-nw', '-n', args.container_name, '--', 'echo -n']) + + core_start_container(name) + if distro == 'arch': - distrobox_run_container('sudo pacman -Sy') - distrobox_run_container('sudo pacman --noconfirm -S --needed git base-devel') - distrobox_run_container('cd ~; git clone https://aur.archlinux.org/yay.git') - distrobox_run_container('cd ~/yay; makepkg --noconfirm -si') + core_run_container('sudo pacman -Sy') + core_run_container('sudo pacman --noconfirm -Syu --needed git base-devel') + core_run_container('TEMP_DIR="$(mktemp -d)"; cd "${TEMP_DIR}"; git clone https://aur.archlinux.org/yay.git; cd yay; makepkg --noconfirm -si; rm -rf "${TEMP_DIR}"') -distrobox_get_output = lambda cmd: subprocess.run(['distrobox-enter', '-nw', '-n', args.container_name, '--', 'sh', '-c', cmd], - stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode('UTF-8').strip() +core_get_output = lambda cmd: subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode('UTF-8').strip() -def distrobox_run_container(cmd): - subprocess.call(['podman', 'exec', '-u', os.environ['USER'], '-it', args.container_name, 'sh', '-c', cmd]) +host_get_output = lambda cmd: subprocess.run(['bash', '-c', cmd], + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode('UTF-8').strip() -def distrobox_install_pkg(pkg): +core_get_retcode = lambda cmd: subprocess.run(['podman', 'exec', '--user', getpass.getuser(), '-it', args.container_name, 'bash', '-c', cmd], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode + +def core_run_container(cmd): + if os.getcwd() == os.path.expanduser('~') or os.getcwd().startswith(os.path.expanduser('~') + '/'): + subprocess.call(['podman', 'exec', '--user', getpass.getuser(), '-w', os.getcwd(), '-it', args.container_name, 'bash', '-c', cmd]) + +def core_install_pkg(pkg): if args.distro == 'fedora-rawhide': if args.noconfirm == True: - distrobox_run_container(f'sudo dnf -y install {pkg}') + core_run_container(f'sudo dnf -y install {pkg}') else: - distrobox_run_container(f'sudo dnf install {pkg}') + core_run_container(f'sudo dnf install {pkg}') elif args.distro == 'arch': - if distrobox_get_output('yay --version') == '': - distrobox_run_container('sudo pacman -Sy') - distrobox_run_container('sudo pacman --noconfirm -S --needed git base-devel') - distrobox_run_container('git clone https://aur.archlinux.org/yay.git') - distrobox_run_container('cd yay; makepkg --noconfirm -si') - distrobox_run_container(f'yay -Sy') + if core_get_retcode('[ -f /usr/bin/yay ]') != 0: + core_run_container('sudo pacman -Sy') + core_run_container('sudo pacman --noconfirm -Syu --needed git base-devel') + core_run_container('TEMP_DIR="$(mktemp -d)"; cd "${TEMP_DIR}"; git clone https://aur.archlinux.org/yay.git; cd yay; makepkg --noconfirm -si; rm -rf "${TEMP_DIR}"') + core_run_container(f'yay -Sy') if args.noconfirm == True: - distrobox_run_container(f'yay --noconfirm -S {pkg}') + core_run_container(f'yay --noconfirm -Syu {pkg}') else: - distrobox_run_container(f'yay -S {pkg}') + core_run_container(f'yay -Syu {pkg}') elif args.distro.startswith('ubuntu-'): - distrobox_run_container(f'sudo apt-get update') + core_run_container(f'sudo apt-get update') if args.noconfirm == True: - distrobox_run_container(f'sudo apt-get install -y {pkg}') + core_run_container(f'sudo apt-get install -y {pkg}') else: - distrobox_run_container(f'sudo apt-get install {pkg}') + core_run_container(f'sudo apt-get install {pkg}') -def distrobox_remove_pkg(pkg): +def core_remove_pkg(pkg): if args.distro == 'fedora-rawhide': if args.noconfirm == True: - distrobox_run_container(f'sudo dnf -y remove {pkg}') + core_run_container(f'sudo dnf -y remove {pkg}') else: - distrobox_run_container(f'sudo dnf remove {pkg}') + core_run_container(f'sudo dnf remove {pkg}') elif args.distro == 'arch': if args.noconfirm == True: - distrobox_run_container(f'sudo pacman --noconfirm -Rcns {pkg}') + core_run_container(f'sudo pacman --noconfirm -Rcns {pkg}') else: - distrobox_run_container(f'sudo pacman -Rcns {pkg}') + core_run_container(f'sudo pacman -Rcns {pkg}') elif args.distro.startswith('ubuntu-'): if args.noconfirm == True: - distrobox_run_container(f'sudo apt-get purge -y {pkg}') + core_run_container(f'sudo apt-get purge -y {pkg}') else: - distrobox_run_container(f'sudo apt-get purge {pkg}') - distrobox_run_container(f'sudo apt-get autoremove --purge -y {pkg}') + core_run_container(f'sudo apt-get purge {pkg}') + core_run_container(f'sudo apt-get autoremove --purge -y {pkg}') -def distrobox_search_pkg(pkg): +def core_search_pkg(pkg): if args.distro == 'fedora-rawhide': - distrobox_run_container(f'dnf search {pkg}') + core_run_container(f'dnf search {pkg}') elif args.distro == 'arch': - distrobox_run_container(f'yay -Sy') - distrobox_run_container(f'yay {pkg}') + core_run_container(f'yay -Sy') + core_run_container(f'yay {pkg}') elif args.distro.startswith('ubuntu-'): - distrobox_run_container(f'sudo apt-get update') - distrobox_run_container(f'apt-cache search {pkg}') + core_run_container(f'sudo apt-get update') + core_run_container(f'apt-cache search {pkg}') -def distrobox_show_pkg(pkg): +def core_show_pkg(pkg): if args.distro == 'fedora-rawhide': - distrobox_run_container(f'dnf info {pkg}') + core_run_container(f'dnf info {pkg}') elif args.distro == 'arch': - distrobox_run_container(f'yay -Sy') - distrobox_run_container(f'yay -Si {pkg}') + core_run_container(f'yay -Sy') + core_run_container(f'yay -Si {pkg}') elif args.distro.startswith('ubuntu-'): - distrobox_run_container(f'sudo apt-get update') - distrobox_run_container(f'apt-cache show {pkg}') - -def install_de(): - if len(args.pkg) == 0: - error('you need to specify a desktop environment (gnome and mate are supported)') - exit() - name = args.pkg[0] - if name == 'mate' or name == 'gnome': - if distro_input == None and cn_input == None: - info(f'using fedora-rawhide instead of {default_distro}, as it\'s recommended for {name}') - info('if you want to use arch, you can specify `--distro arch`') - args.distro = 'fedora-rawhide' - args.container_name = 'fedora-rawhide' - else: - error(f'desktop environment {name} is not supported') - info('gnome and mate are supported') - exit() - if not distrobox_check_container(args.container_name): - distrobox_create_container() - info(f'installing {name} on {args.container_name} (using {args.distro})') - print() - info('ignore any errors after this') - try: - subprocess.run('sudo nearly enter rw', shell=True) - distrobox_run_container('sudo umount /run/systemd/system') - distrobox_run_container('sudo rmdir /run/systemd/system') - distrobox_run_container('sudo ln -s /run/host/run/systemd/system /run/systemd &> /dev/null') - distrobox_run_container('sudo ln -s /run/host/run/dbus/system_bus_socket /run/dbus/ &> /dev/null') - distrobox_run_container('sudo mkdir -p /usr/local/bin') - distrobox_run_container('echo "#!/usr/bin/env bash" | sudo tee /usr/local/bin/disable_dbusactivatable_blend') - distrobox_run_container('echo "if [[ \$DISABLE_REPEAT -ne 1 ]]; then" | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend') - distrobox_run_container('''echo 'while true; do for dir in ${XDG_DATA_DIRS//:/ }; do sudo find $dir/applications -type f -exec sed -i -e '"'s/DBusActivatable=.*//g'"' {} '"';'"'; done; sleep 5; done' | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend''') - distrobox_run_container('echo "else" | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend') - distrobox_run_container('''echo 'for dir in ${XDG_DATA_DIRS//:/ }; do sudo find $dir/applications -type f -exec sed -i -e '"'s/DBusActivatable=.*//g'"' {} '"';'"'; done' | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend''') - distrobox_run_container('echo "fi" | sudo tee -a /usr/local/bin/disable_dbusactivatable_blend') - distrobox_run_container('sudo chmod 755 /usr/local/bin/disable_dbusactivatable_blend') - distrobox_run_container('sudo mkdir -p /etc/xdg/autostart') - distrobox_run_container('echo "[Desktop Entry]" | sudo tee /usr/share/applications/disable_dbusactivatable_blend.desktop') - distrobox_run_container('echo "Version=1.0" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') - distrobox_run_container('echo "Name=Remove DBusActivatable (blend)" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') - distrobox_run_container('echo "Comment=Remove DBusActivatable in all desktop files" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') - distrobox_run_container('echo "Exec=disable_dbusactivatable_blend" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') - distrobox_run_container('echo "Type=Application" | sudo tee -a /usr/share/applications/disable_dbusactivatable_blend.desktop') - distrobox_run_container('sudo chmod 755 /usr/share/applications/disable_dbusactivatable_blend.desktop') - if name == 'mate': - if args.distro == 'fedora-rawhide': - if args.noconfirm == True: - distrobox_run_container(f'sudo dnf --allowerasing -y groupinstall MATE') - else: - distrobox_run_container(f'sudo dnf --allowerasing groupinstall MATE') - elif args.distro == 'arch': - if args.noconfirm == True: - distrobox_run_container(f'sudo pacman --noconfirm -S mate mate-extra') - else: - distrobox_run_container(f'sudo pacman -S mate mate-extra') - elif args.distro.startswith('ubuntu-'): - distrobox_run_container(f'sudo apt-get update') - if args.noconfirm == True: - distrobox_run_container(f'sudo apt-get install -y ubuntu-mate-desktop') - else: - distrobox_run_container(f'sudo apt-get install ubuntu-mate-desktop') - subprocess.run('sudo mkdir -p /usr/local/bin', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "#!/usr/bin/env sh" | sudo tee /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "export GTK_MODULES=" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "export GTK3_MODULES=" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "export LD_PRELOAD=" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "chown -f -R $USER:$USER /tmp/.X11-unix" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "distrobox-enter {args.container_name} -- mate-session" | sudo tee -a /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'sudo chmod 755 /usr/local/bin/{args.container_name}-mate-blend', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "[Desktop Entry]" | sudo tee /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Version=1.0" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Name=MATE ({args.container_name})" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Comment=Use this session to run MATE as your desktop environment" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Exec={args.container_name}-mate-blend" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Type=Application" | sudo tee -a /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'sudo chmod 755 /usr/share/xsessions/mate-blend-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - elif name == 'gnome': - if args.distro == 'fedora-rawhide': - if args.noconfirm == True: - distrobox_run_container(f'sudo dnf --allowerasing -y groupinstall GNOME') - distrobox_run_container(f'sudo dnf --allowerasing -y remove gnome-terminal') - distrobox_run_container(f'sudo dnf --allowerasing -y install gnome-console') - else: - distrobox_run_container(f'sudo dnf --allowerasing groupinstall GNOME') - distrobox_run_container(f'sudo dnf --allowerasing -y remove gnome-terminal') - distrobox_run_container(f'sudo dnf --allowerasing -y install gnome-console') - elif args.distro == 'arch': - if args.noconfirm == True: - distrobox_run_container(f'sudo pacman --noconfirm -S gnome gnome-console') - distrobox_run_container(f'sudo pacman --noconfirm -Rcns gnome-terminal') - else: - distrobox_run_container(f'sudo pacman -S gnome gnome-console') - distrobox_run_container(f'sudo pacman --noconfirm -Rcns gnome-terminal') - elif args.distro.startswith('ubuntu-'): - distrobox_run_container(f'sudo apt-get update') - if args.noconfirm == True: - distrobox_run_container(f'sudo apt-get install -y ubuntu-desktop gnome-console') - distrobox_run_container(f'sudo apt-get purge -y gnome-terminal') - distrobox_run_container(f'sudo apt-get purge -y --auto-remove') - else: - distrobox_run_container(f'sudo apt-get install ubuntu-desktop gnome-console') - distrobox_run_container(f'sudo apt-get purge -y gnome-terminal') - distrobox_run_container(f'sudo apt-get purge -y --auto-remove') - subprocess.run(f'echo "[Desktop Entry]" | sudo tee /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Name=GNOME on Wayland ({args.container_name})" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Comment=This session logs you into GNOME" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Exec=distrobox-enter -n {args.container_name} -- gnome-session --builtin" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Type=Application" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "DesktopNames=GNOME" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "X-GDM-SessionRegisters=true" | sudo tee -a /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "[Desktop Entry]" | sudo tee /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Name=GNOME on Xorg ({args.container_name})" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Comment=This session logs you into GNOME" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Exec=distrobox-enter -n {args.container_name} -- gnome-session --builtin" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "Type=Application" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "DesktopNames=GNOME" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'echo "X-GDM-SessionRegisters=true" | sudo tee -a /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'sudo chmod 755 /usr/share/wayland-sessions/gnome-wayland-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - subprocess.run(f'sudo chmod 755 /usr/share/xsessions/gnome-xorg-{args.container_name}.desktop', stdout=subprocess.DEVNULL, shell=True) - distrobox_run_container('sudo mkdir -p /usr/share/applications /usr/local/share/applications') - distrobox_run_container('''echo 'for dir in /usr/share/applications /usr/local/share/applications; do sudo find $dir -type f -exec sed -i -e '"'s/DBusActivatable=.*//g'"' {} '"';'"'; done' | sudo bash''') - subprocess.run('sudo nearly enter ro', shell=True) - except: - try: - subprocess.call(['sudo', 'nearly', 'enter', 'ro']) - except KeyboardInterrupt: - error('looks like you interrupted blend. your system is currently in read-write mode') - info('run `nearly enter ro\' to enable immutability again') - exit(1) - -def copy_desktop_files(pkg): - distrobox_run_container(f'CONTAINER_ID={args.container_name} distrobox-export --app {pkg} &>/dev/null') - -def del_desktop_files(pkg): - subprocess.call(['distrobox-enter', '-n', args.container_name, '-e', f'sh -c "CONTAINER_ID={args.container_name} distrobox-export --app {pkg} --delete &>/dev/null"']) - -def export_blend(): - for pkg in args.pkg: - copy_desktop_files(pkg) - -def unexport_blend(): - for pkg in args.pkg: - del_desktop_files(pkg) + core_run_container(f'sudo apt-get update') + core_run_container(f'apt-cache show {pkg}') def install_blend(): - if len(args.pkg) != 0: - info('installed binaries can be executed from each container\'s respective terminal') - print() - if len(args.pkg) == 0: error('no packages to install') for pkg in args.pkg: info(f'installing blend {pkg}') - if not distrobox_check_container(args.container_name): - distrobox_create_container() - distrobox_install_pkg(pkg) - copy_desktop_files(pkg) + if not check_container(args.container_name): + core_create_container() + core_install_pkg(pkg) def remove_blend(): if len(args.pkg) == 0: @@ -340,19 +283,18 @@ def remove_blend(): for pkg in args.pkg: info(f'removing blend {pkg}') - if not distrobox_check_container(args.container_name): + if not check_container(args.container_name): error(f"container {args.container_name} doesn't exist") - distrobox_remove_pkg(pkg) - del_desktop_files(pkg) + core_remove_pkg(pkg) def search_blend(): if len(args.pkg) == 0: error('no packages to search for') for pkg in args.pkg: - if not distrobox_check_container(args.container_name): + if not check_container(args.container_name): error(f"container {args.container_name} doesn't exist") - distrobox_search_pkg(pkg) + core_search_pkg(pkg) def show_blend(): if len(args.pkg) == 0: @@ -360,89 +302,99 @@ def show_blend(): for pkg in args.pkg: info(f'info about blend {pkg}') - if not distrobox_check_container(args.container_name): + if not check_container(args.container_name): error(f"container {args.container_name} doesn't exist") - distrobox_show_pkg(pkg) + core_show_pkg(pkg) def sync_blends(): if args.distro == 'fedora-rawhide': - distrobox_run_container(f'dnf makecache') + core_run_container(f'dnf makecache') elif args.distro == 'arch': - distrobox_run_container(f'yay -Syy') + core_run_container(f'yay -Syy') elif args.distro.startswith('ubuntu-'): - distrobox_run_container(f'sudo apt-get update') + core_run_container(f'sudo apt-get update') def update_blends(): if args.distro == 'fedora-rawhide': if args.noconfirm == True: - distrobox_run_container(f'sudo dnf -y upgrade') + core_run_container(f'sudo dnf -y upgrade') else: - distrobox_run_container(f'sudo dnf upgrade') + core_run_container(f'sudo dnf upgrade') elif args.distro == 'arch': if args.noconfirm == True: - distrobox_run_container(f'yay --noconfirm') + core_run_container(f'yay --noconfirm') else: - distrobox_run_container(f'yay') + core_run_container(f'yay') elif args.distro.startswith('ubuntu-'): - distrobox_run_container(f'sudo apt-get update') + core_run_container(f'sudo apt-get update') if args.noconfirm == True: - distrobox_run_container(f'sudo apt-get dist-upgrade -y') + core_run_container(f'sudo apt-get dist-upgrade -y') else: - distrobox_run_container(f'sudo apt-get dist-upgrade') + core_run_container(f'sudo apt-get dist-upgrade') else: error(f'distribution {args.distro} is not supported') -def system_update(): - try: - if args.noconfirm == True: - ret = subprocess.call(['sudo', 'nearly', 'run', '/usr/bin/pacman --noconfirm -Syu']) - else: - ret = subprocess.call(['sudo', 'nearly', 'run', '/usr/bin/pacman -Syu']) - except: - try: - subprocess.call(['sudo', 'nearly', 'enter', 'ro']) - except KeyboardInterrupt: - error('looks like you interrupted blend. your system is currently in read-write mode') - info('run `nearly enter ro\' to enable immutability again') - exit(1) - def enter_container(): - if not distrobox_check_container(args.container_name): - distrobox_create_container() - if os.environ.get('BLEND_COMMAND') == None or os.environ.get('BLEND_COMMAND') == '': - exit(subprocess.call(['distrobox-enter', '-n', args.container_name])) + if os.environ.get('BLEND_NO_CHECK') == None: + if not check_container(args.container_name): + core_create_container() + if check_container_status(args.container_name) != 'running': + core_start_container(args.container_name) + podman_args = [] + sudo = [] + if os.environ.get('SUDO_USER') == None: + podman_args = ['--user', getpass.getuser()] else: - exit(subprocess.call(['distrobox-enter', '-n', args.container_name, '-e', os.environ['BLEND_COMMAND']])) + sudo = ['sudo', '-u', os.environ.get('SUDO_USER'), f'PATH={os.path.expanduser("~/.local/share/bin/blend_bin")}:/usr/bin'] + for name, val in os.environ.items(): + if name not in ['LANG', 'LC_CTYPE', 'PATH', 'HOST', 'HOSTNAME', 'SHELL'] and not name.startswith('_'): + podman_args.append('--env') + podman_args.append(name + '=' + val) + if os.environ.get('BLEND_COMMAND') == None or os.environ.get('BLEND_COMMAND') == '': + if args.pkg == []: + if os.getcwd() == os.path.expanduser('~') or os.getcwd().startswith(os.path.expanduser('~') + '/'): + exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', os.getcwd(), '-it', args.container_name, 'bash'])) + else: + exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', '/run/host' + os.getcwd(), '-it', args.container_name, 'bash'])) + else: + if os.getcwd() == os.path.expanduser('~') or os.getcwd().startswith(os.path.expanduser('~') + '/'): + exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', os.getcwd(), '-it', args.container_name, *args.pkg])) + else: + exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', '/run/host' + os.getcwd(), '-it', args.container_name, *args.pkg])) + else: + if os.getcwd() == os.path.expanduser('~') or os.getcwd().startswith(os.path.expanduser('~') + '/'): + exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', os.getcwd(), '-it', args.container_name, 'bash', '-c', os.environ.get('BLEND_COMMAND')])) + else: + exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', '/run/host' + os.getcwd(), '-it', args.container_name, 'bash'])) def create_container(): for container in args.pkg: args.container_name = container if container in distro_map.keys() and distro_input == None: args.distro = container - distrobox_create_container() - subprocess.call(['distrobox-enter', '-nw', '-n', args.container_name, '--', 'echo -n']) + core_create_container() def remove_container(): for container in args.pkg: info(f'removing container {container}') subprocess.run(['podman', 'stop', container], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) - subprocess.run(['distrobox-rm', container, '--force'], stdout=subprocess.DEVNULL) + subprocess.run(['podman', 'rm', '-f', container], stdout=subprocess.DEVNULL) def start_containers(): _list = subprocess.run(['podman', 'ps', '-a', '--no-trunc', '--size', '--format', '{{.Names}}:{{.Mounts}}'], stdout=subprocess.PIPE).stdout.decode('utf-8').strip() if len(_list) == 0: - info('No containers. Create one by installing a package (`blend install hello`), or manually create one (`blend create-container arch`).') + info('No containers. Create one by installing a package (`blend install hello`), or manually create one (`blend create-container -d arch`).') for container in _list.splitlines(keepends=False): container = container.split(':')[0] info(f'starting container {container}') - subprocess.call(['distrobox-enter', '-n', args.container_name, '--', 'true']) + subprocess.call(['podman', 'start', container]) -if shutil.which('distrobox') is None: - error("distrobox isn't installed, which is a hard dep") +if shutil.which('podman') is None: + error("podman isn't installed, which is a hard dep") exit(1) -if os.geteuid() == 0: +if os.geteuid() == 0 and os.environ['BLEND_ALLOW_ROOT'] == None: error("do not run as root") exit(1) @@ -480,10 +432,7 @@ Here's a list of the supported desktop environments: {colors.bold}help{colors.reset} Show this help message and exit. {colors.bold}version{colors.reset} Show version information and exit. {colors.bold}enter{colors.reset} Enter the container shell. - {colors.bold}export{colors.reset} Export the desktop entry for an installed blend. - {colors.bold}unexport{colors.reset} Unexport the desktop entry for a blend. {colors.bold}install{colors.reset} Install packages inside a container. - {colors.bold}install-de{colors.reset} Install a desktop environment inside a container. {colors.bold}(EXPERIMENTAL){colors.reset} {colors.bold}remove{colors.reset} Remove packages inside a managed container. {colors.bold}create-container{colors.reset} Create a container managed by blend. {colors.bold}remove-container{colors.reset} Remove a container managed by blend. @@ -493,7 +442,6 @@ Here's a list of the supported desktop environments: {colors.bold}search{colors.reset} Search for packages in a managed container. {colors.bold}show{colors.reset} Show details about a package. {colors.bold}update{colors.reset} Update all the packages in a managed container. - {colors.bold}system-update{colors.reset} Update all the system packages. {colors.bold}{colors.fg.purple}options for commands{colors.reset}: {colors.bold}-cn CONTAINER NAME, --container-name CONTAINER NAME{colors.reset} @@ -512,17 +460,13 @@ parser = argparse.ArgumentParser(description=description, usage=argparse.SUPPRES epilog=epilog, formatter_class=argparse.RawTextHelpFormatter) command_map = { 'install': install_blend, 'remove': remove_blend, - 'install-de': install_de, 'enter': enter_container, - 'create-container': create_container, + 'create-container': core_create_container, 'remove-container': remove_container, - 'list-containers': distrobox_list_containers, + 'list-containers': list_containers, 'start-containers': start_containers, 'sync': sync_blends, 'update': update_blends, - 'system-update': system_update, - 'export': export_blend, - 'unexport': unexport_blend, 'search': search_blend, 'show': show_blend, 'help': 'help', diff --git a/blend-files b/blend-files new file mode 100755 index 0000000..f9bca35 --- /dev/null +++ b/blend-files @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 + +import os, sys, yaml, time, getpass, shutil, fileinput, subprocess + +def get_containers(): + container_list = subprocess.run(['sudo', '-u', user, 'podman', 'ps', '-a', '--no-trunc', '--size', '--sort=created', '--format', + '{{.Names}}'], stdout=subprocess.PIPE).stdout.decode('utf-8').strip().split('\n') + + try: + with open(os.path.expanduser('~/.config/blend/config.yaml')) as config_file: + data = yaml.safe_load(config_file) + order = data['container_order'].copy() + order.reverse() + container_list.reverse() + for i in container_list: + if i.strip() not in order: + order.insert(0, i) + for i, o in enumerate(order): + if o not in container_list: + del order[i] + return order + except: + return container_list + +def list_use_container_bin(): + try: + with open(os.path.expanduser('~/.config/blend/config.yaml')) as config_file: + data = yaml.safe_load(config_file) + return data['use_container_bins'] + except: + return [] + +def check_if_present(attr, desktop_str): + for l in desktop_str: + if l.startswith(attr + '='): + return True + return False + +def which(bin): + results = [] + for dir in os.environ.get('PATH').split(':'): + if os.path.isdir(dir): + for i in os.listdir(dir): + if os.path.basename(bin) == i: + results.append(os.path.join(dir, i)) + if results == []: + return None + return results + +def create_container_binaries(): + _binaries = [] + remove_binaries = [] + + for c in _list: + c = c.strip() + for i in con_get_output(c, '''find /usr/bin -type f -printf "%P\n" 2>/dev/null; + find /usr/local/bin -type f -printf "%P\n" 2>/dev/null; + find /usr/sbin -type f -printf "%P\n" 2>/dev/null;''').split('\n'): + i = i.strip() + os.makedirs(os.path.expanduser(f'~/.local/bin/blend_{c}'), exist_ok=True) + i_present = False + orig_which_out = which(os.path.basename(i)) + which_out = None + if orig_which_out != None: + which_out = orig_which_out.copy() + try: + which_out.remove(os.path.expanduser(f'~/.local/bin/blend_bin/{os.path.basename(i)}')) + except ValueError: + pass + if which_out == []: + which_out = None + if which_out != None and os.path.basename(i) not in _exceptions: + i_present = True + + if os.path.basename(i) != 'host-spawn' and i != '' and not i_present: + with open(os.path.expanduser(f'~/.local/bin/blend_{c}/{os.path.basename(i)}.tmp'), 'w') as f: + f.write('#!/bin/sh\n') + f.write(f'# blend container: {i}\n') + if os.path.basename(i) in _exceptions: + f.write(f'# EXCEPTION\n') + f.write(f'BLEND_ALLOW_ROOT= BLEND_NO_CHECK= blend enter -cn {c} -- {i} "$@"\n') + # XXX: make this bit fully atomic + os.chmod(os.path.expanduser(f'~/.local/bin/blend_{c}/{os.path.basename(i)}.tmp'), 0o775) + subprocess.call(['mv', os.path.expanduser(f'~/.local/bin/blend_{c}/{os.path.basename(i)}.tmp'), + os.path.expanduser(f'~/.local/bin/blend_{c}/{os.path.basename(i)}')]) + _binaries.append((c, os.path.basename(os.path.basename(i)))) + + os.makedirs(os.path.expanduser(f'~/.local/bin/blend_bin'), exist_ok=True) + + for c, i in _binaries: + try: + os.symlink(os.path.expanduser(f'~/.local/bin/blend_{c}/{i}'), os.path.expanduser(f'~/.local/bin/blend_bin/{i}')) + except FileExistsError: + if not subprocess.call(['grep', '-q', f'^# container: {c}$', os.path.expanduser(f'~/.local/bin/blend_bin/{i}')]): + os.remove(os.path.expanduser(f'~/.local/bin/blend_bin/{i}')) + os.symlink(os.path.expanduser(f'~/.local/bin/blend_{c}/{i}'), os.path.expanduser(f'~/.local/bin/blend_bin/{i}')) + + for i in remove_binaries: + try: + os.remove(i) + except: + pass + + for b in os.listdir(os.path.expanduser(f'~/.local/bin/blend_bin')): + if [_b for _b in _binaries if _b[1] == b] == []: + os.remove(os.path.join(os.path.expanduser(f'~/.local/bin/blend_bin'), b)) + +def create_container_applications(): + _apps = [] + + os.makedirs(os.path.expanduser(f'~/.local/share/applications'), exist_ok=True) + + for c in _list: + c = c.strip() + for i in con_get_output(c, 'find /usr/share/applications -type f 2>/dev/null; find /usr/local/share/applications -type f 2>/dev/null').split('\n'): + orig_path = i.strip() + i = os.path.basename(orig_path) + i_present = (os.path.isfile(f'/usr/share/applications/{i}') or os.path.isfile(f'/usr/local/share/applications/{i}') + or os.path.isfile(os.path.expanduser(f'~/.local/share/applications/{i}'))) + if i != '' and not i_present: + with open(os.path.expanduser(f'~/.local/share/applications/blend;{i}'), 'w') as f: + _ = con_get_output(c, f"sudo sed -i '/^DBusActivatable=/d' {orig_path}") + _ = con_get_output(c, f"sudo sed -i '/^TryExec=/d' {orig_path}") + contents = con_get_output(c, f'cat {orig_path}') + f.write(contents) + for line in fileinput.input(os.path.expanduser(f'~/.local/share/applications/blend;{i}'), inplace=True): + if line.strip().startswith('Exec='): + line = f'Exec=env BLEND_NO_CHECK= blend enter -cn {c} -- {line[5:]}\n' + elif line.strip().startswith('Icon='): + if '/' in line: + line = line.strip() + _ = con_get_output(c, f"mkdir -p ~/.local/share/blend/icons/file/\"{c}_{i}\"; cp {line[5:]} ~/.local/share/blend/icons/file/\"{c}_{i}\"") + line = f'Icon={os.path.expanduser("~/.local/share/blend/icons/file/" + c + "_" + i + "/" + os.path.basename(line[5:]))}\n' + else: + line = line.strip() + icons = con_get_output(c, f'''find /usr/share/icons /usr/share/pixmaps /var/lib/flatpak/exports/share/icons \\ + -type f -iname "*{line[5:]}*" 2> /dev/null | sort''').split('\r\n') + _ = con_get_output(c, f"mkdir -p ~/.local/share/blend/icons/\"{c}_{i}\"; cp {icons[0]} ~/.local/share/blend/icons/\"{c}_{i}\"") + line = f'Icon={os.path.expanduser("~/.local/share/blend/icons/" + c + "_" + i + "/" + os.path.basename(icons[0]))}\n' + sys.stdout.write(line) + os.chmod(os.path.expanduser(f'~/.local/share/applications/blend;{i}'), 0o775) + _apps.append((c, i)) + del _ + + for a in os.listdir(os.path.expanduser(f'~/.local/share/applications')): + if a.startswith('blend;'): + a = a.removeprefix('blend;') + if [_a for _a in _apps if _a[1] == a] == []: + os.remove(os.path.expanduser(f'~/.local/share/applications/blend;{a}')) + +def create_container_sessions(type='xsessions'): + session_dir = f'/usr/share/{type}' + + os.makedirs('/usr/share/xsessions', exist_ok=True) + + for session in os.listdir(session_dir): + if session.startswith(os.path.join(session_dir, 'blend-')): + os.remove(os.path.join(session_dir, session)) + + for c in _list: + c = c.strip() + for i in con_get_output(c, f'find {session_dir} -type f 2>/dev/null').split('\n'): + orig_path = i.strip() + i = os.path.basename(orig_path) + if i != '': + with open(os.path.expanduser(f'{session_dir}/blend-{c};{i}'), 'w') as f: + contents = con_get_output(c, f'cat {orig_path}') + f.write(contents) + for line in fileinput.input(os.path.expanduser(f'/{session_dir}/blend-{c};{i}'), inplace=True): + if line.strip().startswith('Name'): + name = line.split('=')[1] + line = f'Name=Container {c}: {name}' + elif line.strip().startswith('Exec='): + line = f'Exec=blend enter -cn {c} -- {line[5:]}' + elif line.strip().startswith('TryExec='): + continue + + sys.stdout.write(line) + os.chmod(os.path.expanduser(f'{session_dir}/blend-{c};{i}'), 0o775) + +con_get_output = lambda name, cmd: subprocess.run(['sudo', '-u', user, 'podman', 'exec', '--user', getpass.getuser(), '-it', name, 'bash', '-c', cmd], + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode('UTF-8').strip() + +user = getpass.getuser() + +try: + user = sys.argv[2] +except: + pass + +try: + if sys.argv[1] == 'sessions': + _list = get_containers() + create_container_sessions(type='xsessions') + create_container_sessions(type='wayland-sessions') + exit(0) +except IndexError: + pass + +for c in get_containers(): + c = c.strip() + subprocess.call(['podman', 'start', c]) + +while True: + _list = get_containers() + _exceptions = list_use_container_bin() + + create_container_binaries() + create_container_applications() + time.sleep(6) diff --git a/blend-files.service b/blend-files.service new file mode 100644 index 0000000..41ca88f --- /dev/null +++ b/blend-files.service @@ -0,0 +1,10 @@ +[Unit] +Description=Make container apps accessible + +[Service] +Type=simple +StandardOutput=journal +ExecStart=/usr/bin/blend-files + +[Install] +WantedBy=default.target \ No newline at end of file diff --git a/blend-profiled.sh b/blend-profiled.sh deleted file mode 100644 index 425fae8..0000000 --- a/blend-profiled.sh +++ /dev/null @@ -1,73 +0,0 @@ -# shellcheck shell=sh - -# Expand $PATH to include the directory where blend's package manager shortcuts -# are located. -blend_pkgmanager_bin_path="/blend/pkgmanagers" -if [ -n "${PATH##*${blend_pkgmanager_bin_path}}" ] && [ -n "${PATH##*${blend_pkgmanager_bin_path}:*}" ]; then - export PATH="${blend_pkgmanager_bin_path}:${PATH}" -fi - -# Start all the containers -blend start-containers &>/dev/null || : - -if [[ ! -f "${HOME}/.disable_blend_msg" ]]; then - shell_bold='\033[01m' - shell_color_purple='\033[35m' - shell_reset='\033[0m' - - echo -e "${shell_bold}Welcome to the ${shell_color_purple}blendOS${shell_reset}${shell_bold} shell!" - - echo -e "${shell_reset}note: if you don't want to see this message, you can create a file in your" - echo -e "home directory named .disable_blend_msg${shell_bold}" - echo - - echo -e 'Here are some useful commands:' - echo -e "${shell_reset}To install a package in an Arch container:${shell_bold}" - echo -e " blend install " - echo -e "${shell_reset}To remove a package in an Arch container:${shell_bold}" - echo -e " blend remove " - echo -e "${shell_reset}To install a package in a Fedora container:${shell_bold}" - echo -e " blend install -d fedora-rawhide" - echo -e "${shell_reset}To enter a Fedora container:${shell_bold}" - echo -e " blend enter -cn fedora-rawhide" - echo -e "${shell_reset}To update all the system packages:${shell_bold}" - echo -e " blend system-update${shell_reset} (do not use 'pacman -Syu', as it will only update the" - echo -e " packages in the Arch container)" - echo -e "${shell_reset}To list all the containers:${shell_bold}" - echo -e " blend list-containers" - - echo -e "Keep in mind that none of these commands should be run as root." - - echo - echo -e "${shell_reset}Most apps installed through blend will automatically appear in the applications" - echo -e "list. However, if they don't, you can always manually export them by running:" - echo -e " ${shell_bold}blend export [DESKTOP FILE WITHOUT EXTENSION]${shell_reset}" - echo - - echo -e "You can always specify a distribution (default is arch) by appending ${shell_bold}" - echo -e "--distro=[DISTRO]${shell_reset} to the end of a blend command." - echo -e "(for example: ${shell_bold}blend install hello --distro=ubuntu-22.10)${shell_reset}" - echo - echo -e "Here are the supported distributions:" - echo -e "${shell_bold}1.${shell_reset} arch (default)" - echo -e "${shell_bold}2.${shell_reset} fedora-rawhide" - echo -e "${shell_bold}3.${shell_reset} ubuntu-22.04" - echo -e "${shell_bold}4.${shell_reset} ubuntu-22.10" - echo -e "You can also specify a custom container name (default is the distro's name) by" - echo -e "appending ${shell_bold}--container-name=[CONTAINER]${shell_reset} to the end of a blend command." - - echo - echo -e "You can also use these packages managers directly:" - echo -e "${shell_bold}1.${shell_reset} pacman/yay (distro: arch)" - echo -e "${shell_bold}2.${shell_reset} apt/apt-get (distro: ubuntu-22.10)" - echo -e "${shell_bold}3.${shell_reset} dnf/yum (distro: fedora-rawhide)" - echo -e "However, you'll need to manually export the desktop files" - echo -e "for packages installed this way, by running:" - echo -e " ${shell_bold}blend export [DESKTOP FILE WITHOUT EXTENSION] --distro=[DISTRO]${shell_reset}" - - echo - echo -e "For more information about ${shell_color_purple}blend${shell_reset}${shell_bold}, run:" - echo -e " blend help" - - echo -e "${shell_reset}" -fi diff --git a/blend-settings/.gitignore b/blend-settings/.gitignore new file mode 100644 index 0000000..abc6562 --- /dev/null +++ b/blend-settings/.gitignore @@ -0,0 +1,4 @@ +dist/ +node_modules/ +package-lock.json +web-server/data/applist.json \ No newline at end of file diff --git a/LICENSE b/blend-settings/LICENSE similarity index 99% rename from LICENSE rename to blend-settings/LICENSE index f288702..00f4acf 100644 --- a/LICENSE +++ b/blend-settings/LICENSE @@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - Copyright (C) + Grapes + Copyright (C) 2023 Rudra Saraswat This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - Copyright (C) + Copyright (C) 2023 Rudra Saraswat This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. @@ -671,4 +671,4 @@ into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read -. +. \ No newline at end of file diff --git a/blend-settings/build/icon.png b/blend-settings/build/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a2fce7522f00900bfb8feefa7df78265ce151e6e GIT binary patch literal 16225 zcmZ|W1yCGK^dRuX-GT*&5Zv9}T|#gR8r}EAV;0bsW1sO?T34A?Bv;+VPBu7~t7Z3;* zSOp&TAIn~VH3U~VB`JhGXk;uz{80F7JP=5jQBG1!!*lT@$16K__N{Ym&+C-CWuYIn zuqL)PSv>jOq=g(+X$kH34{~$1xKi#^RuV{l9mzQ1AI(PFIp#9ho=`11hOe^M$nI7N zj9$Ed>=U}Iz4BIpbN807vY)cYAO8DBX!X33*)Dr}ba}guJWjVYncFV*u#6w|%A4{g!GTFif@+e<>p>A#rs*z1%7L@tvf^q$(%u zUqnjOZB6iJdpI`D7+ljJ3(i7B$RK{gOSg>F+@}uN>gU!$)7{>&S4JK;=RLZLgUY| zgj2(7Rq=+z^KN(@*IC^~$uWms#vHJo^o{n;udi0u(=(N+~A0TXgES0`a}LJnmg1=tWiZXa<*^s@;|NOz;lMDF;8x`hOIGuF?ICS00P9 zgUKg}UYbo)7JXRYU7^Qn(&IVIxTmP*BVaL-UOv+sN8;0?Hx@+Z+K7k~^kHH%IyktX zooame>ax|bgPvREt{W{b{wnX<{*O;uGCqx_vF z)$eb`0y{%=9#ys$@&IJzFDJn=#3i6(eHtfXt#R^9^7s+LUCT@$D)oX#-5w+Ii z&`B%JhTN3Z0w$8IM#SsdgJJ#h_m3ZbP0zcQZBV0&K;v7h#3!!LpHw=^@yf!iDJFvP z{(#)8Lr7?`@}zwUFX)b8DQrVu0YdLPoHv`lRe1J4==MSdIQXF{gV zi5DwnDtVxoN3SkrT^gI8b@f_^mJQ?og+zDid#X`YN{Shk&cQz%Cq-iHe$h8w0AbRi z(Mi>9Kb|nFukpR8ipQ9d%SwB*&bXOS(hVxT-B$@BPQK5|DWAmdk&QNp(y6=6(ov5t z$r@@?DylS7=-8!xaUyOpt!^@94tqK0W+Oa)g5Wi)RZJaxzswytm9YfQghjz)Oh$8L z;i0yN`qKLim-bh>iF`o7a4Jdv&ym)M?X-V}?A0&UCrMJedM~3_2 z5xzp;UR+Fe%Z~|KP^!za;DPJ6g78hImd7qa>MhX5i8cvHn*O%*iBc!4rsN~W^!`5e#Wra?0WV)Xk5;zRK@-^=w zOg{hqh8yCn$yc5Y$0jj?$uV}eu6(*ZmXXM4!4^%!w1a2Ak(xaZEr8&TevArt0TEf9 z)@_LHLvT>qf0Ygz@TNEl`;Ct&FH)2@g;6S00YF3jf+%6aaUU~&Z@X-pL5^)Ep2qB) zqfawm%5`J)$mvH&_4v*&svS0ZI#XpK@UC*O?U@KiRTK(SmOLnbM%9{@i&ouG6VRvC zO1FyR&}@guko%MDW&chyzg24Wg9G1r(W0td33ty){?E_~3_Y@j*EJr}*CPok7mwZ3 zkrl*?x_h1Hn3=O#fE$si{nCNH!YE=2_BJfv53k?;(jtXETUPzg7-b9Fl7AM}|s)zMQ!c8u3`a(5P5{BmrbREP#2 zYukStdF1F(PAnar+}!S)B}(DRmrjmFryXGtaoaZ0P?xs;1ca*clo8Fwl9xr&7syY^ z2zFr$UMzYEX^qdiq_tK2Dl4_7=Y7#o{o5GofVAd(0@uZb4_b(*#V{v>-tmX1#;65A z*BBk+Kb<*OM@lh1*2=0(C0+l#2MLMCPf&`km7mStsW*%t)sOkmsp$VE$i}klA^0{! zd5Cp`*go{t{0}>Sg)qs1Z2zj-SRJ*<>*l-n6M^uYqM;p_uj2P8^JCJEqse>A7T{6m ztRTNe0FUr32L!o$B#BAC9gX&odg_?JY+lv){9Zr8SDX6!MO{w6w04Cxsf^iGf%CRx zndJuBG=zefL^gJKBPKP4nRj%K)(t6lH3?yi&-;9nlAKsdxVR5no?@7#7y@xb3ds=C1|f+21+Es0ewMAY7$bIn@Z)In zAvF1PuGRRT!b{S#3(_!?mVe9*PcrNogNme$N^8FyrW?61=H>sqtAI7*SWx6ANp3c3WON=dBvH z*;}Fmo&+pglgr#7ua;JW)zu@Xz0d`U-cpJ{t{FmWv^MXpT{5lYm%QUQvo{0^VeGu9 zaauf&NP!*_F}3F&m1oY@hTO8^!_q{h>jrPVHYTddtt{#@!nNo$6i4g7Wv{USG^xiu zh}W0w3;J-kcg+)Fq^4}RnaE0EUo*ImY(B#zX#{c zY}h013$pgZQ@K9c$j&8nywwK?eAK>;SRk_qh;+=YlA*_v4}FJanl{OvvZJCibjs#_EnvBSNN>jv?0?o^|HR+%12wZZ9zjeNQ2M?YXo(2e|DBW0#{I z{P}WKD*w4GRk>7)IQG)P26Pw?AuD7wK7Z9k5V&>VasBA+*XI9H^!ULqu^q9|dW+=V zuvH&vYz2C@!s;ljKR^Wa1Q#XnSgX!x)E}2v zI%4t*Y*Cr|=TgF(Z;XjlwLfhXgjB|-uOgjErY2k4AAuVK+Oh()r7?CX!hx7K=03tr z$YYnXUPYsxL*uHUWvs7U`$wbTkxdAO#xjG83D;0wabk*+OR0wfU#$C7hlkrPgzUYG zBM~}Ev(T8%w$TlP6Q7Gq702ekC<{vNNh_URnbn7M{1ZXw@l~lPJK|tW$tYSZDy>s? z-(GIePKmPy-?!aoaRpN$<@o6>{+zj`gOh7_`38pM)b9E=r>zJQ9^SB$CZirxcn~KhL&mC_1I*QQIPoN!q=QrTl9+n}iJ6aMUl>R$ltezHrTJYzvNT-#By7khY}M^g zmvj#}`-9%Kx%y{%iHzT+oVVC&la4b79=mw)qS!EarzE; z$C`7X7_g;h7uF!I$M#^=*0O4Fh93G(zNFaah~Bt62)0HEl^Qkq;)B6VP%D^f_GZ@6 z^{X^P1?6!pziGWn;8sqprIjhg(`qoYH+eE=y~B5?{HMW}XLLjjR1IP%fH_FCTqRK#Y6vF3J-?MI z7xWV8uPp-mi0w_&hW#&V5fEHo1z`EteX?=uI3YX_DCU%Bbt{n|8B*R$@zC$GmNHP& znt_uL0d&Y^+4kOgZ1M@^asHqg*!4gPE4AGO7jrlO)QoMED7 zm-Xnr0zzp_g9YZY?#b8;2foF>t>X=NxMblc94cA1BEfb|+^S#m!wJ5}gyq|E#jsaE zJ1Kk3PGI#snI!PTeWlusB=1Z!wucr?&HH+x6{uIpCra-=;gH049DYxj$b?p(?{VIS z-jS9s)xW4F%>RC@98Kz;%XXYC&I#2v1Y(H&QQNelW=?x(Q=y4cK1FM^6Sq*cv_M0x zY@3^B*|urH(Zyzz;M9K}&-_QNst)2tNH;s%yT&63t z+>vNqMR)=;5Jf2F=7Av?dixFCclVq)sM=~LN^(howR{I&X3{dj`E|MOaopH*g0rWX zEZu}L?=c(pixwWCKNOq1B>fc>OwcoQx{e%wbuRfOD9Swq-IoSIt{eT){UK5i>2BGO zV|AHyb;!TEHo&M!4sB3f;s>YKw-u}F?nrN?>mDGHL7kooAu4;8uu9QJ;o!ge-1?xp zP3w%qCwiJ7u#G+hE9TYBwZTtv5!{`f>Di_VRte+XTTgxgNYY3No^(Qj|wI}9#iOY1otIUVnb6s-v<`8E94 zu3q55^*gXAX`MOZdcJJs^>w!=XkK*Abep-MYo%j0k}J!2JK6mNdNoXzi(CPG^9DZC zMSG5R8a*CjG*4wm31$>aB}Yk+{Sc*E0tCGD<`k`p=|d?knV*spuk#UvdN696e>0cQ zqf}Sb0bpE|0HX?8E`w!tZ>NAW`5U&qN?V+OE}l#dlPdtSlz$gK3KK zc~k>`Q&N3OjeH}7HlAq2$i5_lNo0Y|)>wuAmeT-=-)N9)nvPPz{nV?0=qr5w(7D%j znZ3g|?9%C+T9O&hv(kZtiC#kmW&P{R$>Im z`x47>B{yrC-3Pxsm*G6^X43Lr{yomeG~#KDzpW9CmCT0dr4GAyoSZOQC&FAXHi5FL zuH6y52+t;>-SX`%?^Hy>_7W;GjN?$s z5p+fTokx{$KOn}2%&75jX#tsOZqC~Bl~fmY&b!<9i-6}ik%qO;k6BcEnmis{Q-iua*mumTIz$Q0u!`B~E4;FB*NUkCZQ zr&rx$3yJ5YFg?;-NBCzIQu+oA1iMD>=se_t+{^q8n{~m69r`_{`Ce4_9P+$v4(G0p zq8*A!bbk6qrE>zbzOF-|k^GS+t+B_EyM!}}HHrK=RhtqnS~ph)o+h{@eBUm7e;>2< zk+cVYf>S3SI%Yc1a`DR_p+!==_!_G2_kFC<6|Ff(TFTMyofwEoa-HG}sfm54?u5Iw z^4skhxzC_$`Sx=z-Ux*h);cXp>V%!!FGx#_=ypB2Q&E`j8)JO52qe%TSRHr(2;6lU zqq+98DS{w))J|x;>Q2|RXGMQ%kt;E>qWCEpgU4G1l)#-wXFsq&PQ%m?!K@aEsy%Gr zu$IwWO7}L!za^HNYEl#`tlC6AlpgjP<>P*E-}sfQX_drs%l3t≈nSNdTuibcc-M za1TkY{qpC0p%W{NT|t*zK8zl1AX~dLe$ZQ`rQd?@T${~EQ{M44-Wot?`*;MA*8VcZ z9hj88{n0UVIvYvuCX?olb+=OxPueN@SGcZYIZ$v;q~k|4>Tby}XiXPR@~SJmg=FRi}sox_tA zS&urNY0`y|2Ev$aSuUMG7n@IB>)YgZt<(cfBi@pB`u21 zx9!>dIc?f;p4WAE%$4^F$=JpF7;>QTjnn$}7DgJF4jef)%b@pca-o6yANk9-KsnIt zf-6nVf#IF7Gn8o#L3keryWajx9RZQ78O1pT*(F7?0EX6GldBDEn%91*AnnB)A~^B{ zCrIscQ939!PMWB+^P}eQbAbL!tct@0rjs#)^YOIAkFPNm9qur>)-&z106487W)J5v zqFf?F5Nix9>iYERIXw}@&#<$1;eoz^EJHUn69GsU=s8kowEin|x_KA`Tw`sfego4L z&5!?%76HL89#3btYhpki6cYnD&3VYa20mZXBByW8AZ$ZLKC#}*5x}>~QWATv_%Zk9 zXh^|4g)9;;ki?i~UeIQKCJ+n@5zK%l#zjdsuMSC)f~%5nVzNoq%teS?N;6E# zoFRA6Uu|l2DnjCcq@_;}qz!%SeWBLH9T;B_1Mj&u6wFC`{N8`3k`Tj4q=E%m_Z~zu zh!=+Xs;+8e(h(B3vM^1ylihmA#|`=11|J>od_3)v;s}_`EZ~x|$v5`tx(z=6BN&wAA$I=ueTZ-sp_}@O+z6?I zlmB0HKEd;8z55gs6-2EfU==CfZxo1Y&jcD@GSildrO4&JTm@#e9-sm z1N6by#WlYdvPrC4rk*%cyS6oMyz@hp6x_{|a+t7@4FhHGEwm-8NtP|c4EdTjeWAwb z9W;VI?2e%p$>XhoAM;JtYxxOf?3&X~+w*vbt!W^&470Y=zxj0!fY8rReB}5WCY?G9 z5aN{$ARd5o4$@ygkbd*g;DuVx-EflX0us4zSm)j0wW9*Ad(vh<4=uWt`H6y≥Yng1U@)Z^z&G*F# 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 { + if (terminalWindow.getTitle().startsWith('Creating container: ')) { + dialog.showMessageBox(terminalWindow, { + 'type': 'question', + 'title': 'Are you sure?', + 'message': "A container is being created. Closing this window will prevent you from seeing its creation status. Are you sure?", + 'buttons': [ + 'Yes', + 'No' + ] + }).then((result) => { + if (result.response === 0) { + terminalWindow.hide() + ptyProcess.destroy() + } + }) + } else { + terminalWindow.hide() + ptyProcess.destroy() + } + e.preventDefault() + }) + + terminalWindow.webContents.send("terminal.reset") + + terminalWindow.webContents.send("title", title); + + ptyProcess = pty.spawn('/bin/bash', ['-c', cmd], { + name: "xterm-color", + cols: 80, + rows: 30, + cwd: process.env.HOME, + env: process.env + }); + + ptyProcess.on('data', data => { + if (!terminalWindow.isDestroyed()) { + terminalWindow.webContents.send("terminal.incomingData", data) + } + }); + ptyProcess.on('exit', () => { + if (!terminalWindow.isDestroyed()) { + terminalWindow.webContents.send("terminal.reset") + terminalWindow.hide() + if (title.startsWith('Creating container: ')) { + mainWindow.webContents.send("container-created") + } + } + }) + ipcMain.on("terminal.keystroke", (event, key) => { + ptyProcess.write(key) + }); + ipcMain.on("terminal.resize", (event, size) => { + ptyProcess.resize(size[0], size[1]) + }); +} + +app.whenReady().then(() => { + app.allowRendererProcessReuse = false + + createWindow() + createTerminalWindow() + + ipcMain.on('create-term', (event, data) => { + loadTerminalWindow(data['title'], data['cmd']) + }) + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) + + +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit() +}) \ No newline at end of file diff --git a/blend-settings/package.json b/blend-settings/package.json new file mode 100644 index 0000000..0f547c7 --- /dev/null +++ b/blend-settings/package.json @@ -0,0 +1,28 @@ +{ + "name": "blend-settings", + "version": "1.0.0", + "description": "A settings app for blendOS", + "main": "main.js", + "homepage": "https://blendos.org", + "author": "Rudra Saraswat", + "license": "GPL-3.0-or-later", + "scripts": { + "start": "electron .", + "pack": "electron-builder --dir", + "dist": "electron-builder" + }, + "dependencies": { + "@electron/remote": "^2.0.9", + "@types/jquery": "^3.5.16", + "jquery": "^3.6.3", + "js-yaml": "^4.1.0", + "node-pty": "github:daniel-brenot/node-pty#rust-port", + "sortablejs": "^1.15.0", + "xterm": "^5.1.0", + "xterm-addon-fit": "^0.7.0", + "xterm-addon-ligatures": "^0.6.0" + }, + "devDependencies": { + "electron": "^23.0.0" + } +} diff --git a/blend-settings/src/external/css/bootstrap.min.css b/blend-settings/src/external/css/bootstrap.min.css new file mode 100644 index 0000000..83dc5b7 --- /dev/null +++ b/blend-settings/src/external/css/bootstrap.min.css @@ -0,0 +1,7 @@ +@charset "UTF-8";/*! + * Bootstrap v5.0.0-beta3 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0))}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-font-sans-serif);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y) * -1);margin-right:calc(var(--bs-gutter-x)/ -2);margin-left:calc(var(--bs-gutter-x)/ -2)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x)/ 2);padding-left:calc(var(--bs-gutter-x)/ 2);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.3333333333%}.col-2{flex:0 0 auto;width:16.6666666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.3333333333%}.col-5{flex:0 0 auto;width:41.6666666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.3333333333%}.col-8{flex:0 0 auto;width:66.6666666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.3333333333%}.col-11{flex:0 0 auto;width:91.6666666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.3333333333%}.offset-2{margin-left:16.6666666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.3333333333%}.offset-5{margin-left:41.6666666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.3333333333%}.offset-8{margin-left:66.6666666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.3333333333%}.offset-11{margin-left:91.6666666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.3333333333%}.col-sm-2{flex:0 0 auto;width:16.6666666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.3333333333%}.col-sm-5{flex:0 0 auto;width:41.6666666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.3333333333%}.col-sm-8{flex:0 0 auto;width:66.6666666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.3333333333%}.col-sm-11{flex:0 0 auto;width:91.6666666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.3333333333%}.offset-sm-2{margin-left:16.6666666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.3333333333%}.offset-sm-5{margin-left:41.6666666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.3333333333%}.offset-sm-8{margin-left:66.6666666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.3333333333%}.offset-sm-11{margin-left:91.6666666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.3333333333%}.col-md-2{flex:0 0 auto;width:16.6666666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.3333333333%}.col-md-5{flex:0 0 auto;width:41.6666666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.3333333333%}.col-md-8{flex:0 0 auto;width:66.6666666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.3333333333%}.col-md-11{flex:0 0 auto;width:91.6666666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.3333333333%}.offset-md-2{margin-left:16.6666666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.3333333333%}.offset-md-5{margin-left:41.6666666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.3333333333%}.offset-md-8{margin-left:66.6666666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.3333333333%}.offset-md-11{margin-left:91.6666666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.3333333333%}.col-lg-2{flex:0 0 auto;width:16.6666666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.3333333333%}.col-lg-5{flex:0 0 auto;width:41.6666666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.3333333333%}.col-lg-8{flex:0 0 auto;width:66.6666666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.3333333333%}.col-lg-11{flex:0 0 auto;width:91.6666666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.3333333333%}.offset-lg-2{margin-left:16.6666666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.3333333333%}.offset-lg-5{margin-left:41.6666666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.3333333333%}.offset-lg-8{margin-left:66.6666666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.3333333333%}.offset-lg-11{margin-left:91.6666666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.3333333333%}.col-xl-2{flex:0 0 auto;width:16.6666666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.3333333333%}.col-xl-5{flex:0 0 auto;width:41.6666666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.3333333333%}.col-xl-8{flex:0 0 auto;width:66.6666666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.3333333333%}.col-xl-11{flex:0 0 auto;width:91.6666666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.3333333333%}.offset-xl-2{margin-left:16.6666666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.3333333333%}.offset-xl-5{margin-left:41.6666666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.3333333333%}.offset-xl-8{margin-left:66.6666666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.3333333333%}.offset-xl-11{margin-left:91.6666666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.3333333333%}.col-xxl-2{flex:0 0 auto;width:16.6666666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.3333333333%}.col-xxl-5{flex:0 0 auto;width:41.6666666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.3333333333%}.col-xxl-8{flex:0 0 auto;width:66.6666666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.3333333333%}.col-xxl-11{flex:0 0 auto;width:91.6666666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.3333333333%}.offset-xxl-2{margin-left:16.6666666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.3333333333%}.offset-xxl-5{margin-left:41.6666666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.3333333333%}.offset-xxl-8{margin-left:66.6666666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.3333333333%}.offset-xxl-11{margin-left:91.6666666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:last-child)>:last-child>*{border-bottom-color:currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-striped>tbody>tr:nth-of-type(odd){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{max-width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);padding:1rem .75rem}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754;padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545;padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu{top:0;right:auto;left:100%}.dropend .dropdown-menu[data-bs-popper]{margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu{top:0;right:100%;left:auto}.dropstart .dropdown-menu[data-bs-popper]{margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:last-of-type{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast:not(.showing):not(.show){opacity:0}.toast.hide{display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1060;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid #d8d8d8;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1040;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-header{display:flex;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.offcanvas-backdrop::before{position:fixed;top:0;left:0;z-index:1039;width:100vw;height:100vh;content:"";background-color:rgba(0,0,0,.5)}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:calc(3 / 4 * 100%)}.ratio-16x9{--bs-aspect-ratio:calc(9 / 16 * 100%)}.ratio-21x9{--bs-aspect-ratio:calc(9 / 21 * 100%)}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{color:#0d6efd!important}.text-secondary{color:#6c757d!important}.text-success{color:#198754!important}.text-info{color:#0dcaf0!important}.text-warning{color:#ffc107!important}.text-danger{color:#dc3545!important}.text-light{color:#f8f9fa!important}.text-dark{color:#212529!important}.text-white{color:#fff!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-reset{color:inherit!important}.bg-primary{background-color:#0d6efd!important}.bg-secondary{background-color:#6c757d!important}.bg-success{background-color:#198754!important}.bg-info{background-color:#0dcaf0!important}.bg-warning{background-color:#ffc107!important}.bg-danger{background-color:#dc3545!important}.bg-light{background-color:#f8f9fa!important}.bg-dark{background-color:#212529!important}.bg-body{background-color:#fff!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/blend-settings/src/external/js/bootstrap.bundle.min.js b/blend-settings/src/external/js/bootstrap.bundle.min.js new file mode 100644 index 0000000..2168d63 --- /dev/null +++ b/blend-settings/src/external/js/bootstrap.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v5.0.0-beta3 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t},e=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i="#"+i.split("#")[1]),e=i&&"#"!==i?i.trim():null}return e},i=t=>{const i=e(t);return i&&document.querySelector(i)?i:null},s=t=>{const i=e(t);return i?document.querySelector(i):null},n=t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const s=Number.parseFloat(e),n=Number.parseFloat(i);return s||n?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0},o=t=>{t.dispatchEvent(new Event("transitionend"))},r=t=>(t[0]||t).nodeType,a=(t,e)=>{let i=!1;const s=e+5;t.addEventListener("transitionend",(function e(){i=!0,t.removeEventListener("transitionend",e)})),setTimeout(()=>{i||o(t)},s)},l=(t,e,i)=>{Object.keys(i).forEach(s=>{const n=i[s],o=e[s],a=o&&r(o)?"element":null==(l=o)?""+l:{}.toString.call(l).match(/\s([a-z]+)/i)[1].toLowerCase();var l;if(!new RegExp(n).test(a))throw new TypeError(t.toUpperCase()+": "+`Option "${s}" provided type "${a}" `+`but expected type "${n}".`)})},c=t=>{if(!t)return!1;if(t.style&&t.parentNode&&t.parentNode.style){const e=getComputedStyle(t),i=getComputedStyle(t.parentNode);return"none"!==e.display&&"none"!==i.display&&"hidden"!==e.visibility}return!1},d=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),h=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?h(t.parentNode):null},f=()=>function(){},u=t=>t.offsetHeight,p=()=>{const{jQuery:t}=window;return t&&!document.body.hasAttribute("data-bs-no-jquery")?t:null},g=()=>"rtl"===document.documentElement.dir,m=(t,e)=>{var i;i=()=>{const i=p();if(i){const s=i.fn[t];i.fn[t]=e.jQueryInterface,i.fn[t].Constructor=e,i.fn[t].noConflict=()=>(i.fn[t]=s,e.jQueryInterface)}},"loading"===document.readyState?document.addEventListener("DOMContentLoaded",i):i()},_=new Map;var b={set(t,e,i){_.has(t)||_.set(t,new Map);const s=_.get(t);s.has(e)||0===s.size?s.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(t,e)=>_.has(t)&&_.get(t).get(e)||null,remove(t,e){if(!_.has(t))return;const i=_.get(t);i.delete(e),0===i.size&&_.delete(t)}};const v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,E={};let T=1;const A={mouseenter:"mouseover",mouseleave:"mouseout"},L=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${T++}`||t.uidEvent||T++}function k(t){const e=O(t);return t.uidEvent=e,E[e]=E[e]||{},E[e]}function D(t,e,i=null){const s=Object.keys(t);for(let n=0,o=s.length;n{!function(t,e,i,s){const n=e[i]||{};Object.keys(n).forEach(o=>{if(o.includes(s)){const s=n[o];S(t,e,i,s.originalHandler,s.delegationSelector)}})}(t,l,i,e.slice(1))});const d=l[r]||{};Object.keys(d).forEach(i=>{const s=i.replace(w,"");if(!a||e.includes(s)){const e=d[i];S(t,l,r,e.originalHandler,e.delegationSelector)}})},trigger(t,e,i){if("string"!=typeof e||!t)return null;const s=p(),n=e.replace(y,""),o=e!==n,r=L.has(n);let a,l=!0,c=!0,d=!1,h=null;return o&&s&&(a=s.Event(e,i),s(t).trigger(a),l=!a.isPropagationStopped(),c=!a.isImmediatePropagationStopped(),d=a.isDefaultPrevented()),r?(h=document.createEvent("HTMLEvents"),h.initEvent(n,l,!0)):h=new CustomEvent(e,{bubbles:l,cancelable:!0}),void 0!==i&&Object.keys(i).forEach(t=>{Object.defineProperty(h,t,{get:()=>i[t]})}),d&&h.preventDefault(),c&&t.dispatchEvent(h),h.defaultPrevented&&void 0!==a&&a.preventDefault(),h}};class j{constructor(t){(t="string"==typeof t?document.querySelector(t):t)&&(this._element=t,b.set(this._element,this.constructor.DATA_KEY,this))}dispose(){b.remove(this._element,this.constructor.DATA_KEY),this._element=null}static getInstance(t){return b.get(t,this.DATA_KEY)}static get VERSION(){return"5.0.0-beta3"}}class P extends j{static get DATA_KEY(){return"bs.alert"}close(t){const e=t?this._getRootElement(t):this._element,i=this._triggerCloseEvent(e);null===i||i.defaultPrevented||this._removeElement(e)}_getRootElement(t){return s(t)||t.closest(".alert")}_triggerCloseEvent(t){return N.trigger(t,"close.bs.alert")}_removeElement(t){if(t.classList.remove("show"),!t.classList.contains("fade"))return void this._destroyElement(t);const e=n(t);N.one(t,"transitionend",()=>this._destroyElement(t)),a(t,e)}_destroyElement(t){t.parentNode&&t.parentNode.removeChild(t),N.trigger(t,"closed.bs.alert")}static jQueryInterface(t){return this.each((function(){let e=b.get(this,"bs.alert");e||(e=new P(this)),"close"===t&&e[t](this)}))}static handleDismiss(t){return function(e){e&&e.preventDefault(),t.close(this)}}}N.on(document,"click.bs.alert.data-api",'[data-bs-dismiss="alert"]',P.handleDismiss(new P)),m("alert",P);class I extends j{static get DATA_KEY(){return"bs.button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){let e=b.get(this,"bs.button");e||(e=new I(this)),"toggle"===t&&e[t]()}))}}function M(t){return"true"===t||"false"!==t&&(t===Number(t).toString()?Number(t):""===t||"null"===t?null:t)}function R(t){return t.replace(/[A-Z]/g,t=>"-"+t.toLowerCase())}N.on(document,"click.bs.button.data-api",'[data-bs-toggle="button"]',t=>{t.preventDefault();const e=t.target.closest('[data-bs-toggle="button"]');let i=b.get(e,"bs.button");i||(i=new I(e)),i.toggle()}),m("button",I);const B={setDataAttribute(t,e,i){t.setAttribute("data-bs-"+R(e),i)},removeDataAttribute(t,e){t.removeAttribute("data-bs-"+R(e))},getDataAttributes(t){if(!t)return{};const e={};return Object.keys(t.dataset).filter(t=>t.startsWith("bs")).forEach(i=>{let s=i.replace(/^bs/,"");s=s.charAt(0).toLowerCase()+s.slice(1,s.length),e[s]=M(t.dataset[i])}),e},getDataAttribute:(t,e)=>M(t.getAttribute("data-bs-"+R(e))),offset(t){const e=t.getBoundingClientRect();return{top:e.top+document.body.scrollTop,left:e.left+document.body.scrollLeft}},position:t=>({top:t.offsetTop,left:t.offsetLeft})},H={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter(t=>t.matches(e)),parents(t,e){const i=[];let s=t.parentNode;for(;s&&s.nodeType===Node.ELEMENT_NODE&&3!==s.nodeType;)s.matches(e)&&i.push(s),s=s.parentNode;return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]}},W={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},U={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},$="next",F="prev",z="left",K="right";class Y extends j{constructor(t,e){super(t),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(e),this._indicatorsElement=H.findOne(".carousel-indicators",this._element),this._touchSupported="ontouchstart"in document.documentElement||navigator.maxTouchPoints>0,this._pointerEvent=Boolean(window.PointerEvent),this._addEventListeners()}static get Default(){return W}static get DATA_KEY(){return"bs.carousel"}next(){this._isSliding||this._slide($)}nextWhenVisible(){!document.hidden&&c(this._element)&&this.next()}prev(){this._isSliding||this._slide(F)}pause(t){t||(this._isPaused=!0),H.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(o(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null}cycle(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))}to(t){this._activeElement=H.findOne(".active.carousel-item",this._element);const e=this._getItemIndex(this._activeElement);if(t>this._items.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,"slid.bs.carousel",()=>this.to(t));if(e===t)return this.pause(),void this.cycle();const i=t>e?$:F;this._slide(i,this._items[t])}dispose(){N.off(this._element,".bs.carousel"),this._items=null,this._config=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null,super.dispose()}_getConfig(t){return t={...W,...t},l("carousel",t,U),t}_handleSwipe(){const t=Math.abs(this.touchDeltaX);if(t<=40)return;const e=t/this.touchDeltaX;this.touchDeltaX=0,e&&this._slide(e>0?K:z)}_addEventListeners(){this._config.keyboard&&N.on(this._element,"keydown.bs.carousel",t=>this._keydown(t)),"hover"===this._config.pause&&(N.on(this._element,"mouseenter.bs.carousel",t=>this.pause(t)),N.on(this._element,"mouseleave.bs.carousel",t=>this.cycle(t))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()}_addTouchEventListeners(){const t=t=>{!this._pointerEvent||"pen"!==t.pointerType&&"touch"!==t.pointerType?this._pointerEvent||(this.touchStartX=t.touches[0].clientX):this.touchStartX=t.clientX},e=t=>{this.touchDeltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this.touchStartX},i=t=>{!this._pointerEvent||"pen"!==t.pointerType&&"touch"!==t.pointerType||(this.touchDeltaX=t.clientX-this.touchStartX),this._handleSwipe(),"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(t=>this.cycle(t),500+this._config.interval))};H.find(".carousel-item img",this._element).forEach(t=>{N.on(t,"dragstart.bs.carousel",t=>t.preventDefault())}),this._pointerEvent?(N.on(this._element,"pointerdown.bs.carousel",e=>t(e)),N.on(this._element,"pointerup.bs.carousel",t=>i(t)),this._element.classList.add("pointer-event")):(N.on(this._element,"touchstart.bs.carousel",e=>t(e)),N.on(this._element,"touchmove.bs.carousel",t=>e(t)),N.on(this._element,"touchend.bs.carousel",t=>i(t)))}_keydown(t){/input|textarea/i.test(t.target.tagName)||("ArrowLeft"===t.key?(t.preventDefault(),this._slide(z)):"ArrowRight"===t.key&&(t.preventDefault(),this._slide(K)))}_getItemIndex(t){return this._items=t&&t.parentNode?H.find(".carousel-item",t.parentNode):[],this._items.indexOf(t)}_getItemByOrder(t,e){const i=t===$,s=t===F,n=this._getItemIndex(e),o=this._items.length-1;if((s&&0===n||i&&n===o)&&!this._config.wrap)return e;const r=(n+(s?-1:1))%this._items.length;return-1===r?this._items[this._items.length-1]:this._items[r]}_triggerSlideEvent(t,e){const i=this._getItemIndex(t),s=this._getItemIndex(H.findOne(".active.carousel-item",this._element));return N.trigger(this._element,"slide.bs.carousel",{relatedTarget:t,direction:e,from:s,to:i})}_setActiveIndicatorElement(t){if(this._indicatorsElement){const e=H.findOne(".active",this._indicatorsElement);e.classList.remove("active"),e.removeAttribute("aria-current");const i=H.find("[data-bs-target]",this._indicatorsElement);for(let e=0;e{r.classList.remove(h,f),r.classList.add("active"),s.classList.remove("active",f,h),this._isSliding=!1,setTimeout(()=>{N.trigger(this._element,"slid.bs.carousel",{relatedTarget:r,direction:p,from:o,to:l})},0)}),a(s,t)}else s.classList.remove("active"),r.classList.add("active"),this._isSliding=!1,N.trigger(this._element,"slid.bs.carousel",{relatedTarget:r,direction:p,from:o,to:l});c&&this.cycle()}}_directionToOrder(t){return[K,z].includes(t)?g()?t===K?F:$:t===K?$:F:t}_orderToDirection(t){return[$,F].includes(t)?g()?t===$?z:K:t===$?K:z:t}static carouselInterface(t,e){let i=b.get(t,"bs.carousel"),s={...W,...B.getDataAttributes(t)};"object"==typeof e&&(s={...s,...e});const n="string"==typeof e?e:s.slide;if(i||(i=new Y(t,s)),"number"==typeof e)i.to(e);else if("string"==typeof n){if(void 0===i[n])throw new TypeError(`No method named "${n}"`);i[n]()}else s.interval&&s.ride&&(i.pause(),i.cycle())}static jQueryInterface(t){return this.each((function(){Y.carouselInterface(this,t)}))}static dataApiClickHandler(t){const e=s(this);if(!e||!e.classList.contains("carousel"))return;const i={...B.getDataAttributes(e),...B.getDataAttributes(this)},n=this.getAttribute("data-bs-slide-to");n&&(i.interval=!1),Y.carouselInterface(e,i),n&&b.get(e,"bs.carousel").to(n),t.preventDefault()}}N.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",Y.dataApiClickHandler),N.on(window,"load.bs.carousel.data-api",()=>{const t=H.find('[data-bs-ride="carousel"]');for(let e=0,i=t.length;et===this._element);null!==n&&o.length&&(this._selector=n,this._triggerArray.push(e))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}static get Default(){return q}static get DATA_KEY(){return"bs.collapse"}toggle(){this._element.classList.contains("show")?this.hide():this.show()}show(){if(this._isTransitioning||this._element.classList.contains("show"))return;let t,e;this._parent&&(t=H.find(".show, .collapsing",this._parent).filter(t=>"string"==typeof this._config.parent?t.getAttribute("data-bs-parent")===this._config.parent:t.classList.contains("collapse")),0===t.length&&(t=null));const i=H.findOne(this._selector);if(t){const s=t.find(t=>i!==t);if(e=s?b.get(s,"bs.collapse"):null,e&&e._isTransitioning)return}if(N.trigger(this._element,"show.bs.collapse").defaultPrevented)return;t&&t.forEach(t=>{i!==t&&X.collapseInterface(t,"hide"),e||b.set(t,"bs.collapse",null)});const s=this._getDimension();this._element.classList.remove("collapse"),this._element.classList.add("collapsing"),this._element.style[s]=0,this._triggerArray.length&&this._triggerArray.forEach(t=>{t.classList.remove("collapsed"),t.setAttribute("aria-expanded",!0)}),this.setTransitioning(!0);const o="scroll"+(s[0].toUpperCase()+s.slice(1)),r=n(this._element);N.one(this._element,"transitionend",()=>{this._element.classList.remove("collapsing"),this._element.classList.add("collapse","show"),this._element.style[s]="",this.setTransitioning(!1),N.trigger(this._element,"shown.bs.collapse")}),a(this._element,r),this._element.style[s]=this._element[o]+"px"}hide(){if(this._isTransitioning||!this._element.classList.contains("show"))return;if(N.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=this._element.getBoundingClientRect()[t]+"px",u(this._element),this._element.classList.add("collapsing"),this._element.classList.remove("collapse","show");const e=this._triggerArray.length;if(e>0)for(let t=0;t{this.setTransitioning(!1),this._element.classList.remove("collapsing"),this._element.classList.add("collapse"),N.trigger(this._element,"hidden.bs.collapse")}),a(this._element,i)}setTransitioning(t){this._isTransitioning=t}dispose(){super.dispose(),this._config=null,this._parent=null,this._triggerArray=null,this._isTransitioning=null}_getConfig(t){return(t={...q,...t}).toggle=Boolean(t.toggle),l("collapse",t,V),t}_getDimension(){return this._element.classList.contains("width")?"width":"height"}_getParent(){let{parent:t}=this._config;r(t)?void 0===t.jquery&&void 0===t[0]||(t=t[0]):t=H.findOne(t);const e=`[data-bs-toggle="collapse"][data-bs-parent="${t}"]`;return H.find(e,t).forEach(t=>{const e=s(t);this._addAriaAndCollapsedClass(e,[t])}),t}_addAriaAndCollapsedClass(t,e){if(!t||!e.length)return;const i=t.classList.contains("show");e.forEach(t=>{i?t.classList.remove("collapsed"):t.classList.add("collapsed"),t.setAttribute("aria-expanded",i)})}static collapseInterface(t,e){let i=b.get(t,"bs.collapse");const s={...q,...B.getDataAttributes(t),..."object"==typeof e&&e?e:{}};if(!i&&s.toggle&&"string"==typeof e&&/show|hide/.test(e)&&(s.toggle=!1),i||(i=new X(t,s)),"string"==typeof e){if(void 0===i[e])throw new TypeError(`No method named "${e}"`);i[e]()}}static jQueryInterface(t){return this.each((function(){X.collapseInterface(this,t)}))}}N.on(document,"click.bs.collapse.data-api",'[data-bs-toggle="collapse"]',(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=B.getDataAttributes(this),s=i(this);H.find(s).forEach(t=>{const i=b.get(t,"bs.collapse");let s;i?(null===i._parent&&"string"==typeof e.parent&&(i._config.parent=e.parent,i._parent=i._getParent()),s="toggle"):s=e,X.collapseInterface(t,s)})})),m("collapse",X);var Q="top",G="bottom",Z="right",J="left",tt=[Q,G,Z,J],et=tt.reduce((function(t,e){return t.concat([e+"-start",e+"-end"])}),[]),it=[].concat(tt,["auto"]).reduce((function(t,e){return t.concat([e,e+"-start",e+"-end"])}),[]),st=["beforeRead","read","afterRead","beforeMain","main","afterMain","beforeWrite","write","afterWrite"];function nt(t){return t?(t.nodeName||"").toLowerCase():null}function ot(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function rt(t){return t instanceof ot(t).Element||t instanceof Element}function at(t){return t instanceof ot(t).HTMLElement||t instanceof HTMLElement}function lt(t){return"undefined"!=typeof ShadowRoot&&(t instanceof ot(t).ShadowRoot||t instanceof ShadowRoot)}var ct={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},s=e.attributes[t]||{},n=e.elements[t];at(n)&&nt(n)&&(Object.assign(n.style,i),Object.keys(s).forEach((function(t){var e=s[t];!1===e?n.removeAttribute(t):n.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var s=e.elements[t],n=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});at(s)&&nt(s)&&(Object.assign(s.style,o),Object.keys(n).forEach((function(t){s.removeAttribute(t)})))}))}},requires:["computeStyles"]};function dt(t){return t.split("-")[0]}function ht(t){var e=t.getBoundingClientRect();return{width:e.width,height:e.height,top:e.top,right:e.right,bottom:e.bottom,left:e.left,x:e.left,y:e.top}}function ft(t){var e=ht(t),i=t.offsetWidth,s=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-s)<=1&&(s=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:s}}function ut(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&<(i)){var s=e;do{if(s&&t.isSameNode(s))return!0;s=s.parentNode||s.host}while(s)}return!1}function pt(t){return ot(t).getComputedStyle(t)}function gt(t){return["table","td","th"].indexOf(nt(t))>=0}function mt(t){return((rt(t)?t.ownerDocument:t.document)||window.document).documentElement}function _t(t){return"html"===nt(t)?t:t.assignedSlot||t.parentNode||(lt(t)?t.host:null)||mt(t)}function bt(t){return at(t)&&"fixed"!==pt(t).position?t.offsetParent:null}function vt(t){for(var e=ot(t),i=bt(t);i&>(i)&&"static"===pt(i).position;)i=bt(i);return i&&("html"===nt(i)||"body"===nt(i)&&"static"===pt(i).position)?e:i||function(t){for(var e=-1!==navigator.userAgent.toLowerCase().indexOf("firefox"),i=_t(t);at(i)&&["html","body"].indexOf(nt(i))<0;){var s=pt(i);if("none"!==s.transform||"none"!==s.perspective||"paint"===s.contain||-1!==["transform","perspective"].indexOf(s.willChange)||e&&"filter"===s.willChange||e&&s.filter&&"none"!==s.filter)return i;i=i.parentNode}return null}(t)||e}function yt(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}var wt=Math.max,Et=Math.min,Tt=Math.round;function At(t,e,i){return wt(t,Et(e,i))}function Lt(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function Ot(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}var kt={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,s=t.name,n=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=dt(i.placement),l=yt(a),c=[J,Z].indexOf(a)>=0?"height":"width";if(o&&r){var d=function(t,e){return Lt("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:Ot(t,tt))}(n.padding,i),h=ft(o),f="y"===l?Q:J,u="y"===l?G:Z,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],g=r[l]-i.rects.reference[l],m=vt(o),_=m?"y"===l?m.clientHeight||0:m.clientWidth||0:0,b=p/2-g/2,v=d[f],y=_-h[c]-d[u],w=_/2-h[c]/2+b,E=At(v,w,y),T=l;i.modifiersData[s]=((e={})[T]=E,e.centerOffset=E-w,e)}},effect:function(t){var e=t.state,i=t.options.element,s=void 0===i?"[data-popper-arrow]":i;null!=s&&("string"!=typeof s||(s=e.elements.popper.querySelector(s)))&&ut(e.elements.popper,s)&&(e.elements.arrow=s)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]},Dt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function xt(t){var e,i=t.popper,s=t.popperRect,n=t.placement,o=t.offsets,r=t.position,a=t.gpuAcceleration,l=t.adaptive,c=t.roundOffsets,d=!0===c?function(t){var e=t.x,i=t.y,s=window.devicePixelRatio||1;return{x:Tt(Tt(e*s)/s)||0,y:Tt(Tt(i*s)/s)||0}}(o):"function"==typeof c?c(o):o,h=d.x,f=void 0===h?0:h,u=d.y,p=void 0===u?0:u,g=o.hasOwnProperty("x"),m=o.hasOwnProperty("y"),_=J,b=Q,v=window;if(l){var y=vt(i),w="clientHeight",E="clientWidth";y===ot(i)&&"static"!==pt(y=mt(i)).position&&(w="scrollHeight",E="scrollWidth"),y=y,n===Q&&(b=G,p-=y[w]-s.height,p*=a?1:-1),n===J&&(_=Z,f-=y[E]-s.width,f*=a?1:-1)}var T,A=Object.assign({position:r},l&&Dt);return a?Object.assign({},A,((T={})[b]=m?"0":"",T[_]=g?"0":"",T.transform=(v.devicePixelRatio||1)<2?"translate("+f+"px, "+p+"px)":"translate3d("+f+"px, "+p+"px, 0)",T)):Object.assign({},A,((e={})[b]=m?p+"px":"",e[_]=g?f+"px":"",e.transform="",e))}var Ct={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,s=i.gpuAcceleration,n=void 0===s||s,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:dt(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:n};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,xt(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,xt(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}},St={passive:!0},Nt={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,s=t.options,n=s.scroll,o=void 0===n||n,r=s.resize,a=void 0===r||r,l=ot(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,St)})),a&&l.addEventListener("resize",i.update,St),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,St)})),a&&l.removeEventListener("resize",i.update,St)}},data:{}},jt={left:"right",right:"left",bottom:"top",top:"bottom"};function Pt(t){return t.replace(/left|right|bottom|top/g,(function(t){return jt[t]}))}var It={start:"end",end:"start"};function Mt(t){return t.replace(/start|end/g,(function(t){return It[t]}))}function Rt(t){var e=ot(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Bt(t){return ht(mt(t)).left+Rt(t).scrollLeft}function Ht(t){var e=pt(t),i=e.overflow,s=e.overflowX,n=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+n+s)}function Wt(t,e){var i;void 0===e&&(e=[]);var s=function t(e){return["html","body","#document"].indexOf(nt(e))>=0?e.ownerDocument.body:at(e)&&Ht(e)?e:t(_t(e))}(t),n=s===(null==(i=t.ownerDocument)?void 0:i.body),o=ot(s),r=n?[o].concat(o.visualViewport||[],Ht(s)?s:[]):s,a=e.concat(r);return n?a:a.concat(Wt(_t(r)))}function Ut(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function $t(t,e){return"viewport"===e?Ut(function(t){var e=ot(t),i=mt(t),s=e.visualViewport,n=i.clientWidth,o=i.clientHeight,r=0,a=0;return s&&(n=s.width,o=s.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(r=s.offsetLeft,a=s.offsetTop)),{width:n,height:o,x:r+Bt(t),y:a}}(t)):at(e)?function(t){var e=ht(t);return e.top=e.top+t.clientTop,e.left=e.left+t.clientLeft,e.bottom=e.top+t.clientHeight,e.right=e.left+t.clientWidth,e.width=t.clientWidth,e.height=t.clientHeight,e.x=e.left,e.y=e.top,e}(e):Ut(function(t){var e,i=mt(t),s=Rt(t),n=null==(e=t.ownerDocument)?void 0:e.body,o=wt(i.scrollWidth,i.clientWidth,n?n.scrollWidth:0,n?n.clientWidth:0),r=wt(i.scrollHeight,i.clientHeight,n?n.scrollHeight:0,n?n.clientHeight:0),a=-s.scrollLeft+Bt(t),l=-s.scrollTop;return"rtl"===pt(n||i).direction&&(a+=wt(i.clientWidth,n?n.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(mt(t)))}function Ft(t){return t.split("-")[1]}function zt(t){var e,i=t.reference,s=t.element,n=t.placement,o=n?dt(n):null,r=n?Ft(n):null,a=i.x+i.width/2-s.width/2,l=i.y+i.height/2-s.height/2;switch(o){case Q:e={x:a,y:i.y-s.height};break;case G:e={x:a,y:i.y+i.height};break;case Z:e={x:i.x+i.width,y:l};break;case J:e={x:i.x-s.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?yt(o):null;if(null!=c){var d="y"===c?"height":"width";switch(r){case"start":e[c]=e[c]-(i[d]/2-s[d]/2);break;case"end":e[c]=e[c]+(i[d]/2-s[d]/2)}}return e}function Kt(t,e){void 0===e&&(e={});var i=e,s=i.placement,n=void 0===s?t.placement:s,o=i.boundary,r=void 0===o?"clippingParents":o,a=i.rootBoundary,l=void 0===a?"viewport":a,c=i.elementContext,d=void 0===c?"popper":c,h=i.altBoundary,f=void 0!==h&&h,u=i.padding,p=void 0===u?0:u,g=Lt("number"!=typeof p?p:Ot(p,tt)),m="popper"===d?"reference":"popper",_=t.elements.reference,b=t.rects.popper,v=t.elements[f?m:d],y=function(t,e,i){var s="clippingParents"===e?function(t){var e=Wt(_t(t)),i=["absolute","fixed"].indexOf(pt(t).position)>=0&&at(t)?vt(t):t;return rt(i)?e.filter((function(t){return rt(t)&&ut(t,i)&&"body"!==nt(t)})):[]}(t):[].concat(e),n=[].concat(s,[i]),o=n[0],r=n.reduce((function(e,i){var s=$t(t,i);return e.top=wt(s.top,e.top),e.right=Et(s.right,e.right),e.bottom=Et(s.bottom,e.bottom),e.left=wt(s.left,e.left),e}),$t(t,o));return r.width=r.right-r.left,r.height=r.bottom-r.top,r.x=r.left,r.y=r.top,r}(rt(v)?v:v.contextElement||mt(t.elements.popper),r,l),w=ht(_),E=zt({reference:w,element:b,strategy:"absolute",placement:n}),T=Ut(Object.assign({},b,E)),A="popper"===d?T:w,L={top:y.top-A.top+g.top,bottom:A.bottom-y.bottom+g.bottom,left:y.left-A.left+g.left,right:A.right-y.right+g.right},O=t.modifiersData.offset;if("popper"===d&&O){var k=O[n];Object.keys(L).forEach((function(t){var e=[Z,G].indexOf(t)>=0?1:-1,i=[Q,G].indexOf(t)>=0?"y":"x";L[t]+=k[i]*e}))}return L}function Yt(t,e){void 0===e&&(e={});var i=e,s=i.placement,n=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?it:l,d=Ft(s),h=d?a?et:et.filter((function(t){return Ft(t)===d})):tt,f=h.filter((function(t){return c.indexOf(t)>=0}));0===f.length&&(f=h);var u=f.reduce((function(e,i){return e[i]=Kt(t,{placement:i,boundary:n,rootBoundary:o,padding:r})[dt(i)],e}),{});return Object.keys(u).sort((function(t,e){return u[t]-u[e]}))}var qt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,s=t.name;if(!e.modifiersData[s]._skip){for(var n=i.mainAxis,o=void 0===n||n,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,d=i.boundary,h=i.rootBoundary,f=i.altBoundary,u=i.flipVariations,p=void 0===u||u,g=i.allowedAutoPlacements,m=e.options.placement,_=dt(m),b=l||(_!==m&&p?function(t){if("auto"===dt(t))return[];var e=Pt(t);return[Mt(t),e,Mt(e)]}(m):[Pt(m)]),v=[m].concat(b).reduce((function(t,i){return t.concat("auto"===dt(i)?Yt(e,{placement:i,boundary:d,rootBoundary:h,padding:c,flipVariations:p,allowedAutoPlacements:g}):i)}),[]),y=e.rects.reference,w=e.rects.popper,E=new Map,T=!0,A=v[0],L=0;L=0,C=x?"width":"height",S=Kt(e,{placement:O,boundary:d,rootBoundary:h,altBoundary:f,padding:c}),N=x?D?Z:J:D?G:Q;y[C]>w[C]&&(N=Pt(N));var j=Pt(N),P=[];if(o&&P.push(S[k]<=0),a&&P.push(S[N]<=0,S[j]<=0),P.every((function(t){return t}))){A=O,T=!1;break}E.set(O,P)}if(T)for(var I=function(t){var e=v.find((function(e){var i=E.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return A=e,"break"},M=p?3:1;M>0&&"break"!==I(M);M--);e.placement!==A&&(e.modifiersData[s]._skip=!0,e.placement=A,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function Vt(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Xt(t){return[Q,Z,G,J].some((function(e){return t[e]>=0}))}var Qt={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,s=e.rects.reference,n=e.rects.popper,o=e.modifiersData.preventOverflow,r=Kt(e,{elementContext:"reference"}),a=Kt(e,{altBoundary:!0}),l=Vt(r,s),c=Vt(a,n,o),d=Xt(l),h=Xt(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:d,hasPopperEscaped:h},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":d,"data-popper-escaped":h})}},Gt={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,s=t.name,n=i.offset,o=void 0===n?[0,0]:n,r=it.reduce((function(t,i){return t[i]=function(t,e,i){var s=dt(t),n=[J,Q].indexOf(s)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*n,[J,Z].indexOf(s)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[s]=r}},Zt={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=zt({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},Jt={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,s=t.name,n=i.mainAxis,o=void 0===n||n,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,d=i.altBoundary,h=i.padding,f=i.tether,u=void 0===f||f,p=i.tetherOffset,g=void 0===p?0:p,m=Kt(e,{boundary:l,rootBoundary:c,padding:h,altBoundary:d}),_=dt(e.placement),b=Ft(e.placement),v=!b,y=yt(_),w="x"===y?"y":"x",E=e.modifiersData.popperOffsets,T=e.rects.reference,A=e.rects.popper,L="function"==typeof g?g(Object.assign({},e.rects,{placement:e.placement})):g,O={x:0,y:0};if(E){if(o||a){var k="y"===y?Q:J,D="y"===y?G:Z,x="y"===y?"height":"width",C=E[y],S=E[y]+m[k],N=E[y]-m[D],j=u?-A[x]/2:0,P="start"===b?T[x]:A[x],I="start"===b?-A[x]:-T[x],M=e.elements.arrow,R=u&&M?ft(M):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},H=B[k],W=B[D],U=At(0,T[x],R[x]),$=v?T[x]/2-j-U-H-L:P-U-H-L,F=v?-T[x]/2+j+U+W+L:I+U+W+L,z=e.elements.arrow&&vt(e.elements.arrow),K=z?"y"===y?z.clientTop||0:z.clientLeft||0:0,Y=e.modifiersData.offset?e.modifiersData.offset[e.placement][y]:0,q=E[y]+$-Y-K,V=E[y]+F-Y;if(o){var X=At(u?Et(S,q):S,C,u?wt(N,V):N);E[y]=X,O[y]=X-C}if(a){var tt="x"===y?Q:J,et="x"===y?G:Z,it=E[w],st=it+m[tt],nt=it-m[et],ot=At(u?Et(st,q):st,it,u?wt(nt,V):nt);E[w]=ot,O[w]=ot-it}}e.modifiersData[s]=O}},requiresIfExists:["offset"]};function te(t,e,i){void 0===i&&(i=!1);var s,n,o=mt(e),r=ht(t),a=at(e),l={scrollLeft:0,scrollTop:0},c={x:0,y:0};return(a||!a&&!i)&&(("body"!==nt(e)||Ht(o))&&(l=(s=e)!==ot(s)&&at(s)?{scrollLeft:(n=s).scrollLeft,scrollTop:n.scrollTop}:Rt(s)),at(e)?((c=ht(e)).x+=e.clientLeft,c.y+=e.clientTop):o&&(c.x=Bt(o))),{x:r.left+l.scrollLeft-c.x,y:r.top+l.scrollTop-c.y,width:r.width,height:r.height}}var ee={placement:"bottom",modifiers:[],strategy:"absolute"};function ie(){for(var t=arguments.length,e=new Array(t),i=0;i"applyStyles"===t.name&&!1===t.enabled);this._popper=re(e,this._menu,i),s&&B.setDataAttribute(this._menu,"popper","static")}"ontouchstart"in document.documentElement&&!t.closest(".navbar-nav")&&[].concat(...document.body.children).forEach(t=>N.on(t,"mouseover",null,(function(){}))),this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.toggle("show"),this._element.classList.toggle("show"),N.trigger(this._element,"shown.bs.dropdown",e)}}hide(){if(this._element.disabled||this._element.classList.contains("disabled")||!this._menu.classList.contains("show"))return;const t={relatedTarget:this._element};N.trigger(this._element,"hide.bs.dropdown",t).defaultPrevented||(this._popper&&this._popper.destroy(),this._menu.classList.toggle("show"),this._element.classList.toggle("show"),B.removeDataAttribute(this._menu,"popper"),N.trigger(this._element,"hidden.bs.dropdown",t))}dispose(){N.off(this._element,".bs.dropdown"),this._menu=null,this._popper&&(this._popper.destroy(),this._popper=null),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_addEventListeners(){N.on(this._element,"click.bs.dropdown",t=>{t.preventDefault(),this.toggle()})}_getConfig(t){if(t={...this.constructor.Default,...B.getDataAttributes(this._element),...t},l("dropdown",t,this.constructor.DefaultType),"object"==typeof t.reference&&!r(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError("dropdown".toUpperCase()+': Option "reference" provided type "object" without a required "getBoundingClientRect" method.');return t}_getMenuElement(){return H.next(this._element,".dropdown-menu")[0]}_getPlacement(){const t=this._element.parentNode;if(t.classList.contains("dropend"))return ue;if(t.classList.contains("dropstart"))return pe;const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?de:ce:e?fe:he}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return"static"===this._config.display&&(t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}static dropdownInterface(t,e){let i=b.get(t,"bs.dropdown");if(i||(i=new _e(t,"object"==typeof e?e:null)),"string"==typeof e){if(void 0===i[e])throw new TypeError(`No method named "${e}"`);i[e]()}}static jQueryInterface(t){return this.each((function(){_e.dropdownInterface(this,t)}))}static clearMenus(t){if(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;if(/input|select|textarea|form/i.test(t.target.tagName))return}const e=H.find('[data-bs-toggle="dropdown"]');for(let i=0,s=e.length;it.composedPath().includes(e)))continue;if("keyup"===t.type&&"Tab"===t.key&&o.contains(t.target))continue}N.trigger(e[i],"hide.bs.dropdown",n).defaultPrevented||("ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>N.off(t,"mouseover",null,(function(){}))),e[i].setAttribute("aria-expanded","false"),s._popper&&s._popper.destroy(),o.classList.remove("show"),e[i].classList.remove("show"),B.removeDataAttribute(o,"popper"),N.trigger(e[i],"hidden.bs.dropdown",n))}}}static getParentFromElement(t){return s(t)||t.parentNode}static dataApiKeydownHandler(t){if(/input|textarea/i.test(t.target.tagName)?"Space"===t.key||"Escape"!==t.key&&("ArrowDown"!==t.key&&"ArrowUp"!==t.key||t.target.closest(".dropdown-menu")):!le.test(t.key))return;if(t.preventDefault(),t.stopPropagation(),this.disabled||this.classList.contains("disabled"))return;const e=_e.getParentFromElement(this),i=this.classList.contains("show");if("Escape"===t.key)return(this.matches('[data-bs-toggle="dropdown"]')?this:H.prev(this,'[data-bs-toggle="dropdown"]')[0]).focus(),void _e.clearMenus();if(!i&&("ArrowUp"===t.key||"ArrowDown"===t.key))return void(this.matches('[data-bs-toggle="dropdown"]')?this:H.prev(this,'[data-bs-toggle="dropdown"]')[0]).click();if(!i||"Space"===t.key)return void _e.clearMenus();const s=H.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",e).filter(c);if(!s.length)return;let n=s.indexOf(t.target);"ArrowUp"===t.key&&n>0&&n--,"ArrowDown"===t.key&&nthis.hide(t)),N.on(this._dialog,"mousedown.dismiss.bs.modal",()=>{N.one(this._element,"mouseup.dismiss.bs.modal",t=>{t.target===this._element&&(this._ignoreBackdropClick=!0)})}),this._showBackdrop(()=>this._showElement(t)))}hide(t){if(t&&t.preventDefault(),!this._isShown||this._isTransitioning)return;if(N.trigger(this._element,"hide.bs.modal").defaultPrevented)return;this._isShown=!1;const e=this._isAnimated();if(e&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),N.off(document,"focusin.bs.modal"),this._element.classList.remove("show"),N.off(this._element,"click.dismiss.bs.modal"),N.off(this._dialog,"mousedown.dismiss.bs.modal"),e){const t=n(this._element);N.one(this._element,"transitionend",t=>this._hideModal(t)),a(this._element,t)}else this._hideModal()}dispose(){[window,this._element,this._dialog].forEach(t=>N.off(t,".bs.modal")),super.dispose(),N.off(document,"focusin.bs.modal"),this._config=null,this._dialog=null,this._backdrop=null,this._isShown=null,this._isBodyOverflowing=null,this._ignoreBackdropClick=null,this._isTransitioning=null,this._scrollbarWidth=null}handleUpdate(){this._adjustDialog()}_getConfig(t){return t={...be,...t},l("modal",t,ve),t}_showElement(t){const e=this._isAnimated(),i=H.findOne(".modal-body",this._dialog);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0,i&&(i.scrollTop=0),e&&u(this._element),this._element.classList.add("show"),this._config.focus&&this._enforceFocus();const s=()=>{this._config.focus&&this._element.focus(),this._isTransitioning=!1,N.trigger(this._element,"shown.bs.modal",{relatedTarget:t})};if(e){const t=n(this._dialog);N.one(this._dialog,"transitionend",s),a(this._dialog,t)}else s()}_enforceFocus(){N.off(document,"focusin.bs.modal"),N.on(document,"focusin.bs.modal",t=>{document===t.target||this._element===t.target||this._element.contains(t.target)||this._element.focus()})}_setEscapeEvent(){this._isShown?N.on(this._element,"keydown.dismiss.bs.modal",t=>{this._config.keyboard&&"Escape"===t.key?(t.preventDefault(),this.hide()):this._config.keyboard||"Escape"!==t.key||this._triggerBackdropTransition()}):N.off(this._element,"keydown.dismiss.bs.modal")}_setResizeEvent(){this._isShown?N.on(window,"resize.bs.modal",()=>this._adjustDialog()):N.off(window,"resize.bs.modal")}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._showBackdrop(()=>{document.body.classList.remove("modal-open"),this._resetAdjustments(),this._resetScrollbar(),N.trigger(this._element,"hidden.bs.modal")})}_removeBackdrop(){this._backdrop.parentNode.removeChild(this._backdrop),this._backdrop=null}_showBackdrop(t){const e=this._isAnimated();if(this._isShown&&this._config.backdrop){if(this._backdrop=document.createElement("div"),this._backdrop.className="modal-backdrop",e&&this._backdrop.classList.add("fade"),document.body.appendChild(this._backdrop),N.on(this._element,"click.dismiss.bs.modal",t=>{this._ignoreBackdropClick?this._ignoreBackdropClick=!1:t.target===t.currentTarget&&("static"===this._config.backdrop?this._triggerBackdropTransition():this.hide())}),e&&u(this._backdrop),this._backdrop.classList.add("show"),!e)return void t();const i=n(this._backdrop);N.one(this._backdrop,"transitionend",t),a(this._backdrop,i)}else if(!this._isShown&&this._backdrop){this._backdrop.classList.remove("show");const i=()=>{this._removeBackdrop(),t()};if(e){const t=n(this._backdrop);N.one(this._backdrop,"transitionend",i),a(this._backdrop,t)}else i()}else t()}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(N.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight;t||(this._element.style.overflowY="hidden"),this._element.classList.add("modal-static");const e=n(this._dialog);N.off(this._element,"transitionend"),N.one(this._element,"transitionend",()=>{this._element.classList.remove("modal-static"),t||(N.one(this._element,"transitionend",()=>{this._element.style.overflowY=""}),a(this._element,e))}),a(this._element,e),this._element.focus()}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight;(!this._isBodyOverflowing&&t&&!g()||this._isBodyOverflowing&&!t&&g())&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),(this._isBodyOverflowing&&!t&&!g()||!this._isBodyOverflowing&&t&&g())&&(this._element.style.paddingRight=this._scrollbarWidth+"px")}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}_checkScrollbar(){const t=document.body.getBoundingClientRect();this._isBodyOverflowing=Math.round(t.left+t.right)t+this._scrollbarWidth),this._setElementAttributes(".sticky-top","marginRight",t=>t-this._scrollbarWidth),this._setElementAttributes("body","paddingRight",t=>t+this._scrollbarWidth)),document.body.classList.add("modal-open")}_setElementAttributes(t,e,i){H.find(t).forEach(t=>{if(t!==document.body&&window.innerWidth>t.clientWidth+this._scrollbarWidth)return;const s=t.style[e],n=window.getComputedStyle(t)[e];B.setDataAttribute(t,e,s),t.style[e]=i(Number.parseFloat(n))+"px"})}_resetScrollbar(){this._resetElementAttributes(".fixed-top, .fixed-bottom, .is-fixed, .sticky-top","paddingRight"),this._resetElementAttributes(".sticky-top","marginRight"),this._resetElementAttributes("body","paddingRight")}_resetElementAttributes(t,e){H.find(t).forEach(t=>{const i=B.getDataAttribute(t,e);void 0===i&&t===document.body?t.style[e]="":(B.removeDataAttribute(t,e),t.style[e]=i)})}_getScrollbarWidth(){const t=document.createElement("div");t.className="modal-scrollbar-measure",document.body.appendChild(t);const e=t.getBoundingClientRect().width-t.clientWidth;return document.body.removeChild(t),e}static jQueryInterface(t,e){return this.each((function(){let i=b.get(this,"bs.modal");const s={...be,...B.getDataAttributes(this),..."object"==typeof t&&t?t:{}};if(i||(i=new ye(this,s)),"string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}N.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=s(this);"A"!==this.tagName&&"AREA"!==this.tagName||t.preventDefault(),N.one(e,"show.bs.modal",t=>{t.defaultPrevented||N.one(e,"hidden.bs.modal",()=>{c(this)&&this.focus()})});let i=b.get(e,"bs.modal");if(!i){const t={...B.getDataAttributes(e),...B.getDataAttributes(this)};i=new ye(e,t)}i.toggle(this)})),m("modal",ye);const we=()=>{const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)},Ee=(t,e,i)=>{const s=we();H.find(t).forEach(t=>{if(t!==document.body&&window.innerWidth>t.clientWidth+s)return;const n=t.style[e],o=window.getComputedStyle(t)[e];B.setDataAttribute(t,e,n),t.style[e]=i(Number.parseFloat(o))+"px"})},Te=(t,e)=>{H.find(t).forEach(t=>{const i=B.getDataAttribute(t,e);void 0===i&&t===document.body?t.style.removeProperty(e):(B.removeDataAttribute(t,e),t.style[e]=i)})},Ae={backdrop:!0,keyboard:!0,scroll:!1},Le={backdrop:"boolean",keyboard:"boolean",scroll:"boolean"};class Oe extends j{constructor(t,e){super(t),this._config=this._getConfig(e),this._isShown=!1,this._addEventListeners()}static get Default(){return Ae}static get DATA_KEY(){return"bs.offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._element.style.visibility="visible",this._config.backdrop&&document.body.classList.add("offcanvas-backdrop"),this._config.scroll||((t=we())=>{document.body.style.overflow="hidden",Ee(".fixed-top, .fixed-bottom, .is-fixed","paddingRight",e=>e+t),Ee(".sticky-top","marginRight",e=>e-t),Ee("body","paddingRight",e=>e+t)})(),this._element.classList.add("offcanvas-toggling"),this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add("show"),setTimeout(()=>{this._element.classList.remove("offcanvas-toggling"),N.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t}),this._enforceFocusOnElement(this._element)},n(this._element)))}hide(){this._isShown&&(N.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(this._element.classList.add("offcanvas-toggling"),N.off(document,"focusin.bs.offcanvas"),this._element.blur(),this._isShown=!1,this._element.classList.remove("show"),setTimeout(()=>{this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._element.style.visibility="hidden",this._config.backdrop&&document.body.classList.remove("offcanvas-backdrop"),this._config.scroll||(document.body.style.overflow="auto",Te(".fixed-top, .fixed-bottom, .is-fixed","paddingRight"),Te(".sticky-top","marginRight"),Te("body","paddingRight")),N.trigger(this._element,"hidden.bs.offcanvas"),this._element.classList.remove("offcanvas-toggling")},n(this._element))))}_getConfig(t){return t={...Ae,...B.getDataAttributes(this._element),..."object"==typeof t?t:{}},l("offcanvas",t,Le),t}_enforceFocusOnElement(t){N.off(document,"focusin.bs.offcanvas"),N.on(document,"focusin.bs.offcanvas",e=>{document===e.target||t===e.target||t.contains(e.target)||t.focus()}),t.focus()}_addEventListeners(){N.on(this._element,"click.dismiss.bs.offcanvas",'[data-bs-dismiss="offcanvas"]',()=>this.hide()),N.on(document,"keydown",t=>{this._config.keyboard&&"Escape"===t.key&&this.hide()}),N.on(document,"click.bs.offcanvas.data-api",t=>{const e=H.findOne(i(t.target));this._element.contains(t.target)||e===this._element||this.hide()})}static jQueryInterface(t){return this.each((function(){const e=b.get(this,"bs.offcanvas")||new Oe(this,"object"==typeof t?t:{});if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}N.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=s(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),d(this))return;N.one(e,"hidden.bs.offcanvas",()=>{c(this)&&this.focus()});const i=H.findOne(".offcanvas.show, .offcanvas-toggling");i&&i!==e||(b.get(e,"bs.offcanvas")||new Oe(e)).toggle(this)})),N.on(window,"load.bs.offcanvas.data-api",()=>{H.find(".offcanvas.show").forEach(t=>(b.get(t,"bs.offcanvas")||new Oe(t)).show())}),m("offcanvas",Oe);const ke=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),De=/^(?:(?:https?|mailto|ftp|tel|file):|[^#&/:?]*(?:[#/?]|$))/i,xe=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Ce=(t,e)=>{const i=t.nodeName.toLowerCase();if(e.includes(i))return!ke.has(i)||Boolean(De.test(t.nodeValue)||xe.test(t.nodeValue));const s=e.filter(t=>t instanceof RegExp);for(let t=0,e=s.length;t{Ce(t,a)||i.removeAttribute(t.nodeName)})}return s.body.innerHTML}const Ne=new RegExp("(^|\\s)bs-tooltip\\S+","g"),je=new Set(["sanitize","allowList","sanitizeFn"]),Pe={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(array|string|function)",container:"(string|element|boolean)",fallbackPlacements:"array",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object",popperConfig:"(null|object|function)"},Ie={AUTO:"auto",TOP:"top",RIGHT:g()?"left":"right",BOTTOM:"bottom",LEFT:g()?"right":"left"},Me={animation:!0,template:'',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:[0,0],container:!1,fallbackPlacements:["top","right","bottom","left"],boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},Re={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"};class Be extends j{constructor(t,e){if(void 0===ae)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.config=this._getConfig(e),this.tip=null,this._setListeners()}static get Default(){return Me}static get NAME(){return"tooltip"}static get DATA_KEY(){return"bs.tooltip"}static get Event(){return Re}static get EVENT_KEY(){return".bs.tooltip"}static get DefaultType(){return Pe}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){if(this._isEnabled)if(t){const e=this._initializeOnDelegatedTarget(t);e._activeTrigger.click=!e._activeTrigger.click,e._isWithActiveTrigger()?e._enter(null,e):e._leave(null,e)}else{if(this.getTipElement().classList.contains("show"))return void this._leave(null,this);this._enter(null,this)}}dispose(){clearTimeout(this._timeout),N.off(this._element,this.constructor.EVENT_KEY),N.off(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this.tip&&this.tip.parentNode&&this.tip.parentNode.removeChild(this.tip),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.config=null,this.tip=null,super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this.isWithContent()||!this._isEnabled)return;const e=N.trigger(this._element,this.constructor.Event.SHOW),i=h(this._element),s=null===i?this._element.ownerDocument.documentElement.contains(this._element):i.contains(this._element);if(e.defaultPrevented||!s)return;const o=this.getTipElement(),r=t(this.constructor.NAME);o.setAttribute("id",r),this._element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&o.classList.add("fade");const l="function"==typeof this.config.placement?this.config.placement.call(this,o,this._element):this.config.placement,c=this._getAttachment(l);this._addAttachmentClass(c);const d=this._getContainer();b.set(o,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||(d.appendChild(o),N.trigger(this._element,this.constructor.Event.INSERTED)),this._popper?this._popper.update():this._popper=re(this._element,o,this._getPopperConfig(c)),o.classList.add("show");const f="function"==typeof this.config.customClass?this.config.customClass():this.config.customClass;f&&o.classList.add(...f.split(" ")),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>{N.on(t,"mouseover",(function(){}))});const u=()=>{const t=this._hoverState;this._hoverState=null,N.trigger(this._element,this.constructor.Event.SHOWN),"out"===t&&this._leave(null,this)};if(this.tip.classList.contains("fade")){const t=n(this.tip);N.one(this.tip,"transitionend",u),a(this.tip,t)}else u()}hide(){if(!this._popper)return;const t=this.getTipElement(),e=()=>{this._isWithActiveTrigger()||("show"!==this._hoverState&&t.parentNode&&t.parentNode.removeChild(t),this._cleanTipClass(),this._element.removeAttribute("aria-describedby"),N.trigger(this._element,this.constructor.Event.HIDDEN),this._popper&&(this._popper.destroy(),this._popper=null))};if(!N.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented){if(t.classList.remove("show"),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>N.off(t,"mouseover",f)),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,this.tip.classList.contains("fade")){const i=n(t);N.one(t,"transitionend",e),a(t,i)}else e();this._hoverState=""}}update(){null!==this._popper&&this._popper.update()}isWithContent(){return Boolean(this.getTitle())}getTipElement(){if(this.tip)return this.tip;const t=document.createElement("div");return t.innerHTML=this.config.template,this.tip=t.children[0],this.tip}setContent(){const t=this.getTipElement();this.setElementContent(H.findOne(".tooltip-inner",t),this.getTitle()),t.classList.remove("fade","show")}setElementContent(t,e){if(null!==t)return"object"==typeof e&&r(e)?(e.jquery&&(e=e[0]),void(this.config.html?e.parentNode!==t&&(t.innerHTML="",t.appendChild(e)):t.textContent=e.textContent)):void(this.config.html?(this.config.sanitize&&(e=Se(e,this.config.allowList,this.config.sanitizeFn)),t.innerHTML=e):t.textContent=e)}getTitle(){let t=this._element.getAttribute("data-bs-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this._element):this.config.title),t}updateAttachment(t){return"right"===t?"end":"left"===t?"start":t}_initializeOnDelegatedTarget(t,e){const i=this.constructor.DATA_KEY;return(e=e||b.get(t.delegateTarget,i))||(e=new this.constructor(t.delegateTarget,this._getDelegateConfig()),b.set(t.delegateTarget,i,e)),e}_getOffset(){const{offset:t}=this.config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{altBoundary:!0,fallbackPlacements:this.config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this.config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:t=>this._handlePopperPlacementChange(t)}],onFirstUpdate:t=>{t.options.placement!==t.placement&&this._handlePopperPlacementChange(t)}};return{...e,..."function"==typeof this.config.popperConfig?this.config.popperConfig(e):this.config.popperConfig}}_addAttachmentClass(t){this.getTipElement().classList.add("bs-tooltip-"+this.updateAttachment(t))}_getContainer(){return!1===this.config.container?document.body:r(this.config.container)?this.config.container:H.findOne(this.config.container)}_getAttachment(t){return Ie[t.toUpperCase()]}_setListeners(){this.config.trigger.split(" ").forEach(t=>{if("click"===t)N.on(this._element,this.constructor.Event.CLICK,this.config.selector,t=>this.toggle(t));else if("manual"!==t){const e="hover"===t?this.constructor.Event.MOUSEENTER:this.constructor.Event.FOCUSIN,i="hover"===t?this.constructor.Event.MOUSELEAVE:this.constructor.Event.FOCUSOUT;N.on(this._element,e,this.config.selector,t=>this._enter(t)),N.on(this._element,i,this.config.selector,t=>this._leave(t))}}),this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this.config.selector?this.config={...this.config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){const t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))}_enter(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusin"===t.type?"focus":"hover"]=!0),e.getTipElement().classList.contains("show")||"show"===e._hoverState?e._hoverState="show":(clearTimeout(e._timeout),e._hoverState="show",e.config.delay&&e.config.delay.show?e._timeout=setTimeout(()=>{"show"===e._hoverState&&e.show()},e.config.delay.show):e.show())}_leave(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusout"===t.type?"focus":"hover"]=e._element.contains(t.relatedTarget)),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState="out",e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(()=>{"out"===e._hoverState&&e.hide()},e.config.delay.hide):e.hide())}_isWithActiveTrigger(){for(const t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1}_getConfig(t){const e=B.getDataAttributes(this._element);return Object.keys(e).forEach(t=>{je.has(t)&&delete e[t]}),t&&"object"==typeof t.container&&t.container.jquery&&(t.container=t.container[0]),"number"==typeof(t={...this.constructor.Default,...e,..."object"==typeof t&&t?t:{}}).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),l("tooltip",t,this.constructor.DefaultType),t.sanitize&&(t.template=Se(t.template,t.allowList,t.sanitizeFn)),t}_getDelegateConfig(){const t={};if(this.config)for(const e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t}_cleanTipClass(){const t=this.getTipElement(),e=t.getAttribute("class").match(Ne);null!==e&&e.length>0&&e.map(t=>t.trim()).forEach(e=>t.classList.remove(e))}_handlePopperPlacementChange(t){const{state:e}=t;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))}static jQueryInterface(t){return this.each((function(){let e=b.get(this,"bs.tooltip");const i="object"==typeof t&&t;if((e||!/dispose|hide/.test(t))&&(e||(e=new Be(this,i)),"string"==typeof t)){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m("tooltip",Be);const He=new RegExp("(^|\\s)bs-popover\\S+","g"),We={...Be.Default,placement:"right",offset:[0,8],trigger:"click",content:"",template:''},Ue={...Be.DefaultType,content:"(string|element|function)"},$e={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"};class Fe extends Be{static get Default(){return We}static get NAME(){return"popover"}static get DATA_KEY(){return"bs.popover"}static get Event(){return $e}static get EVENT_KEY(){return".bs.popover"}static get DefaultType(){return Ue}isWithContent(){return this.getTitle()||this._getContent()}setContent(){const t=this.getTipElement();this.setElementContent(H.findOne(".popover-header",t),this.getTitle());let e=this._getContent();"function"==typeof e&&(e=e.call(this._element)),this.setElementContent(H.findOne(".popover-body",t),e),t.classList.remove("fade","show")}_addAttachmentClass(t){this.getTipElement().classList.add("bs-popover-"+this.updateAttachment(t))}_getContent(){return this._element.getAttribute("data-bs-content")||this.config.content}_cleanTipClass(){const t=this.getTipElement(),e=t.getAttribute("class").match(He);null!==e&&e.length>0&&e.map(t=>t.trim()).forEach(e=>t.classList.remove(e))}static jQueryInterface(t){return this.each((function(){let e=b.get(this,"bs.popover");const i="object"==typeof t?t:null;if((e||!/dispose|hide/.test(t))&&(e||(e=new Fe(this,i),b.set(this,"bs.popover",e)),"string"==typeof t)){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m("popover",Fe);const ze={offset:10,method:"auto",target:""},Ke={offset:"number",method:"string",target:"(string|element)"};class Ye extends j{constructor(t,e){super(t),this._scrollElement="BODY"===this._element.tagName?window:this._element,this._config=this._getConfig(e),this._selector=`${this._config.target} .nav-link, ${this._config.target} .list-group-item, ${this._config.target} .dropdown-item`,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,N.on(this._scrollElement,"scroll.bs.scrollspy",()=>this._process()),this.refresh(),this._process()}static get Default(){return ze}static get DATA_KEY(){return"bs.scrollspy"}refresh(){const t=this._scrollElement===this._scrollElement.window?"offset":"position",e="auto"===this._config.method?t:this._config.method,s="position"===e?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),H.find(this._selector).map(t=>{const n=i(t),o=n?H.findOne(n):null;if(o){const t=o.getBoundingClientRect();if(t.width||t.height)return[B[e](o).top+s,n]}return null}).filter(t=>t).sort((t,e)=>t[0]-e[0]).forEach(t=>{this._offsets.push(t[0]),this._targets.push(t[1])})}dispose(){super.dispose(),N.off(this._scrollElement,".bs.scrollspy"),this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null}_getConfig(e){if("string"!=typeof(e={...ze,..."object"==typeof e&&e?e:{}}).target&&r(e.target)){let{id:i}=e.target;i||(i=t("scrollspy"),e.target.id=i),e.target="#"+i}return l("scrollspy",e,Ke),e}_getScrollTop(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop}_getScrollHeight(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}_getOffsetHeight(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height}_process(){const t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),i=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=i){const t=this._targets[this._targets.length-1];this._activeTarget!==t&&this._activate(t)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(let e=this._offsets.length;e--;)this._activeTarget!==this._targets[e]&&t>=this._offsets[e]&&(void 0===this._offsets[e+1]||t`${e}[data-bs-target="${t}"],${e}[href="${t}"]`),i=H.findOne(e.join(","));i.classList.contains("dropdown-item")?(H.findOne(".dropdown-toggle",i.closest(".dropdown")).classList.add("active"),i.classList.add("active")):(i.classList.add("active"),H.parents(i,".nav, .list-group").forEach(t=>{H.prev(t,".nav-link, .list-group-item").forEach(t=>t.classList.add("active")),H.prev(t,".nav-item").forEach(t=>{H.children(t,".nav-link").forEach(t=>t.classList.add("active"))})})),N.trigger(this._scrollElement,"activate.bs.scrollspy",{relatedTarget:t})}_clear(){H.find(this._selector).filter(t=>t.classList.contains("active")).forEach(t=>t.classList.remove("active"))}static jQueryInterface(t){return this.each((function(){let e=b.get(this,"bs.scrollspy");if(e||(e=new Ye(this,"object"==typeof t&&t)),"string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(window,"load.bs.scrollspy.data-api",()=>{H.find('[data-bs-spy="scroll"]').forEach(t=>new Ye(t,B.getDataAttributes(t)))}),m("scrollspy",Ye);class qe extends j{static get DATA_KEY(){return"bs.tab"}show(){if(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&this._element.classList.contains("active")||d(this._element))return;let t;const e=s(this._element),i=this._element.closest(".nav, .list-group");if(i){const e="UL"===i.nodeName||"OL"===i.nodeName?":scope > li > .active":".active";t=H.find(e,i),t=t[t.length-1]}const n=t?N.trigger(t,"hide.bs.tab",{relatedTarget:this._element}):null;if(N.trigger(this._element,"show.bs.tab",{relatedTarget:t}).defaultPrevented||null!==n&&n.defaultPrevented)return;this._activate(this._element,i);const o=()=>{N.trigger(t,"hidden.bs.tab",{relatedTarget:this._element}),N.trigger(this._element,"shown.bs.tab",{relatedTarget:t})};e?this._activate(e,e.parentNode,o):o()}_activate(t,e,i){const s=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?H.children(e,".active"):H.find(":scope > li > .active",e))[0],o=i&&s&&s.classList.contains("fade"),r=()=>this._transitionComplete(t,s,i);if(s&&o){const t=n(s);s.classList.remove("show"),N.one(s,"transitionend",r),a(s,t)}else r()}_transitionComplete(t,e,i){if(e){e.classList.remove("active");const t=H.findOne(":scope > .dropdown-menu .active",e.parentNode);t&&t.classList.remove("active"),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}t.classList.add("active"),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),u(t),t.classList.contains("fade")&&t.classList.add("show"),t.parentNode&&t.parentNode.classList.contains("dropdown-menu")&&(t.closest(".dropdown")&&H.find(".dropdown-toggle").forEach(t=>t.classList.add("active")),t.setAttribute("aria-expanded",!0)),i&&i()}static jQueryInterface(t){return this.each((function(){const e=b.get(this,"bs.tab")||new qe(this);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){t.preventDefault(),(b.get(this,"bs.tab")||new qe(this)).show()})),m("tab",qe);const Ve={animation:"boolean",autohide:"boolean",delay:"number"},Xe={animation:!0,autohide:!0,delay:5e3};class Qe extends j{constructor(t,e){super(t),this._config=this._getConfig(e),this._timeout=null,this._setListeners()}static get DefaultType(){return Ve}static get Default(){return Xe}static get DATA_KEY(){return"bs.toast"}show(){if(N.trigger(this._element,"show.bs.toast").defaultPrevented)return;this._clearTimeout(),this._config.animation&&this._element.classList.add("fade");const t=()=>{this._element.classList.remove("showing"),this._element.classList.add("show"),N.trigger(this._element,"shown.bs.toast"),this._config.autohide&&(this._timeout=setTimeout(()=>{this.hide()},this._config.delay))};if(this._element.classList.remove("hide"),u(this._element),this._element.classList.add("showing"),this._config.animation){const e=n(this._element);N.one(this._element,"transitionend",t),a(this._element,e)}else t()}hide(){if(!this._element.classList.contains("show"))return;if(N.trigger(this._element,"hide.bs.toast").defaultPrevented)return;const t=()=>{this._element.classList.add("hide"),N.trigger(this._element,"hidden.bs.toast")};if(this._element.classList.remove("show"),this._config.animation){const e=n(this._element);N.one(this._element,"transitionend",t),a(this._element,e)}else t()}dispose(){this._clearTimeout(),this._element.classList.contains("show")&&this._element.classList.remove("show"),N.off(this._element,"click.dismiss.bs.toast"),super.dispose(),this._config=null}_getConfig(t){return t={...Xe,...B.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}},l("toast",t,this.constructor.DefaultType),t}_setListeners(){N.on(this._element,"click.dismiss.bs.toast",'[data-bs-dismiss="toast"]',()=>this.hide())}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){let e=b.get(this,"bs.toast");if(e||(e=new Qe(this,"object"==typeof t&&t)),"string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return m("toast",Qe),{Alert:P,Button:I,Carousel:Y,Collapse:X,Dropdown:_e,Modal:ye,Offcanvas:Oe,Popover:Fe,ScrollSpy:Ye,Tab:qe,Toast:Qe,Tooltip:Be}})); +//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/blend-settings/src/index.html b/blend-settings/src/index.html new file mode 100644 index 0000000..1d73363 --- /dev/null +++ b/blend-settings/src/index.html @@ -0,0 +1,64 @@ + + + + Settings + + + + + + + + + +
+
+
+ + +
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/blend-settings/src/internal/css/common.css b/blend-settings/src/internal/css/common.css new file mode 100644 index 0000000..47188f8 --- /dev/null +++ b/blend-settings/src/internal/css/common.css @@ -0,0 +1,98 @@ +body { + margin: 0 !important; + background-color: rgb(36, 36, 48); + color: rgba(240, 240, 255, 1); + overflow: hidden; +} + +.btn-dark { + background-color: rgb(36, 36, 48); +} + +.btn-dark:focus { + background-color: rgb(36, 36, 48); +} + +.btn-dark:hover { + background-color: rgb(32, 32, 44); +} + +.topnav { + display: flex; + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); +} + +.list-group-item { + border: 1px solid rgba(255, 255, 255, 0.05); +} + +.form-control { + border: 0; + background-color: rgba(255, 255, 255, 0.05) !important; + color: white !important; +} + +.form-check-input:not(:checked) { + background-color: rgba(255, 255, 255, 0.05) !important; +} + +select option { + color: white; + background-color: rgb(36, 36, 48); + border: 0; +} + +.form-group { + margin-top: 8px; +} + +#webview { + overflow-y: scroll !important; + position: fixed; + width: 100%; + height: calc(100% - 100px); +} + +#webview::-webkit-scrollbar-track +{ + background-color: rgb(36, 36, 48); + border-radius: 10px; +} + +#webview::-webkit-scrollbar +{ + width: 5px; + background-color: rgb(36, 36, 48); +} + +#webview::-webkit-scrollbar-thumb +{ + background-color: rgb(61, 61, 74); + border-radius: 10px; +} + +.form-check-input { + position: absolute; + font-size: 1.5em; + top: 0.67em; +} + +.list-group-item { + background-color: rgb(29, 29, 37); + color: #fff; +} + +-webkit-scrollbar-track +{ + box-shadow: inset 0 0 6px rgba(0,0,0,0.3); + background-color: #F5F5F5; +} + +-webkit-scrollbar +{ + width: 10px; + background-color: #F5F5F5; +} \ No newline at end of file diff --git a/blend-settings/src/internal/js/containers.js b/blend-settings/src/internal/js/containers.js new file mode 100644 index 0000000..2fdb870 --- /dev/null +++ b/blend-settings/src/internal/js/containers.js @@ -0,0 +1,184 @@ +var term + +function open_container(name) { + ipc.send("create-term", { 'title': `Container: ${name}`, 'cmd': `blend enter -cn ${name}` }); +} + +function create_container () { + container_name = $('#inputContainerName').val() + if (!(/^[\w\-\.]+$/.test(container_name))) { + $('#inputContainerName').get(0).setCustomValidity('Container name can only contain alphanumeric characters and dashes (no spaces allowed).') + $('#inputContainerName').get(0).reportValidity(); + return + } + container_distro = $('#inputContainerDistro').val().toLowerCase().replace(' ', '-') + ipc.send("create-term", { 'title': `Creating container: ${container_name}`, + 'cmd': `blend create-container -cn ${container_name} -d ${container_distro} \ + && echo 'created container successfully (exiting automatically in 5 seconds)' \ + || echo 'container creation failed (exiting automatically in 5 seconds)'; + sleep 5` }); + ipc.on('container-created', () => { + worker.postMessage('update-list') + }) +} + +async function remove_container (name) { + let rm_worker = new Worker( + `data:text/javascript, + require('child_process').spawnSync('podman', ['stop', '-t', '0', '${name}'], { encoding: 'utf8' }) + require('child_process').spawnSync('podman', ['rm', '-f', '${name}'], { encoding: 'utf8' }) + postMessage('') + ` + ) + rm_worker.onmessage = e => worker.postMessage('update-list') +} + +window.worker = new Worker( + `data:text/javascript, + function list_containers() { + let container_list = require('child_process').spawnSync('podman', ['ps', '-a', '--no-trunc', '--size', '--format', '{{.Names}}'], { encoding: 'utf8' }).stdout.split(/\\r?\\n/).filter(Boolean).reverse(); + if (require('fs').existsSync(require('path').join(require('os').homedir(), '.config/blend/config.yaml'), 'utf8')) { + try { + let fileContents = require('fs').readFileSync(require('path').join(require('os').homedir(), '.config/blend/config.yaml'), 'utf8') + let data = require('js-yaml').load(fileContents); + new_container_list = data['container_order'] + container_list.forEach(container => { + if (!new_container_list.includes(container)) { + new_container_list.push(container) + } + }); + new_container_list = new_container_list.filter(container => container_list.includes(container)) + new_container_list.filter((item, index) => arr.indexOf(item) === index) + data = { + container_order: [...new_container_list], + use_container_bins: [] + }; + contents = require('js-yaml').dump(data) + require('fs').writeFileSync(require('path').join(require('os').homedir(), '.config/blend/config.yaml'), contents, 'utf8') + return new_container_list + } catch (e) { + let data = { + container_order: [...container_list], + use_container_bins: [] + }; + contents = require('js-yaml').dump(data) + require('fs').writeFileSync(require('path').join(require('os').homedir(), '.config/blend/config.yaml'), contents, 'utf8') + + return container_list + } + } else { + let data = { + container_order: [...container_list], + use_container_bins: [] + }; + require('fs').mkdirSync(require('path').join(require('os').homedir(), '.config/blend'), { recursive: true }); + contents = require('js-yaml').dump(data) + require('fs').writeFileSync(require('path').join(require('os').homedir(), '.config/blend/config.yaml'), contents, 'utf8') + + return container_list + } + } + + function truncateText(text, maxLength=30) { + let truncated = text + + if (truncated.length > maxLength) { + truncated = truncated.substr(0, maxLength) + '...'; + } + + return truncated; + } + + function update_container_list() { + container_list = list_containers() + + container_list_html_default = \` +
+
+
+ No containers present. +

Create one from below.

+
+
+
+ \` + + container_list_html = '' + + container_list.forEach(container => { + container_list_html += \` +
+
+
+ + + + \${container} +
+
+ + + + + + + + + + +
+
+
+ \` + }); + + if (container_list.length == 0) { + postMessage(container_list_html_default) + } else { + postMessage(container_list_html) + } + } + + self.onmessage = msg => { + switch(msg) { + case 'update-list': + update_container_list() + break; + } + update_container_list() + } + ` +); + +worker.postMessage('update-list') + +worker.onmessage = function (event) { + window.data = event.data + if (event.data.includes('bi-grip-vertical')) { + $('#container-list').addClass('sortable') + $('#container-list').html(event.data) + } else { + $('#container-list').removeClass('sortable') + $('#container-list').html(event.data) + } + + $('.sortable').each((i, e) => { + Sortable.create(e, { + animation: 100, + onEnd: () => { + let container_list = [] + $('.container_name').each((i, e) => { + container_list.push(e.innerText) + }) + let data = { + container_order: [...container_list], + use_container_bins: [] + }; + require('fs').mkdirSync(require('path').join(require('os').homedir(), '.config/blend'), { recursive: true }); + contents = require('js-yaml').dump(data) + require('fs').writeFileSync(require('path').join(require('os').homedir(), '.config/blend/config.yaml'), contents, 'utf8') + } + }); + e.addEventListener("dragstart", e => e.dataTransfer.setDragImage(new Image(), 0, 0), false); + }) +} \ No newline at end of file diff --git a/blend-settings/src/internal/js/generic_page.js b/blend-settings/src/internal/js/generic_page.js new file mode 100644 index 0000000..cddedd0 --- /dev/null +++ b/blend-settings/src/internal/js/generic_page.js @@ -0,0 +1,15 @@ +if (typeof variable !== typeof undefined) { + Sortable = undefined +} else { + window.Sortable = require('sortablejs') +} + +function truncateText(text, maxLength=30) { + let truncated = text + + if (truncated.length > maxLength) { + truncated = truncated.substr(0, maxLength) + '...'; + } + + return truncated; +} \ No newline at end of file diff --git a/blend-settings/src/internal/js/overlay.js b/blend-settings/src/internal/js/overlay.js new file mode 100644 index 0000000..62625ac --- /dev/null +++ b/blend-settings/src/internal/js/overlay.js @@ -0,0 +1,132 @@ +function rollback() { + let rollback_worker = new Worker( + `data:text/javascript, + let s = require('child_process').spawnSync('pkexec', ['blend-system', 'rollback']).status + if (s === 0) { + postMessage('success') + } else { + postMessage('failure') + } + ` + ) + rollback_worker.onmessage = e => { + if (e.data == 'success') { + document.getElementById('rollback-btn').outerHTML = + '' + } else { + document.getElementById('rollback-btn').outerHTML = + '' + setTimeout(() => document.getElementById('rollback-btn').outerHTML = + '', 2000) + } + } +} + +function undo_rollback() { + let undo_rollback_worker = new Worker( + `data:text/javascript, + let s = require('child_process').spawnSync('pkexec', ['rm', '-f', '/blend/states/.load_prev_state']).status + if (s === 0) { + postMessage('success') + } else { + postMessage('failure') + } + ` + ) + undo_rollback_worker.onmessage = e => { + if (e.data == 'success') { + document.getElementById('rollback-btn').outerHTML = + '' + } else { + document.getElementById('rollback-btn').outerHTML = + '' + setTimeout(() => document.getElementById('rollback-btn').outerHTML = + '', 2000) + } + } +} + +function save_state() { + let save_state_worker = new Worker( + `data:text/javascript, + let s = require('child_process').spawnSync('pkexec', ['blend-system', 'save-state']).status + if (s === 0) { + postMessage('success') + } else { + postMessage('failure') + } + ` + ) + save_state_worker.onmessage = e => { + if (e.data == 'success') { + document.getElementById('save-state-btn').outerHTML = + '' + setTimeout(() => document.getElementById('save-state-btn').outerHTML = + '', 2000) + } else { + document.getElementById('save-state-btn').outerHTML = + '' + setTimeout(() => document.getElementById('save-state-btn').outerHTML = + '', 2000) + } + } +} + +function check_rollback() { + if (require('fs').existsSync('/blend/states/.load_prev_state')) { + document.getElementById('rollback-btn').outerHTML = + '' + } else { + document.getElementById('rollback-btn').outerHTML = + '' + } +} + +function check_state_creation() { + if (require('fs').existsSync('/blend/states/.disable_states')) { + document.getElementById('automatic-state-toggle').setAttribute('checked', '') + } +} + +check_state_creation() +check_rollback() + +$('#automatic-state-toggle').on('change', () => { + if (!document.getElementById('automatic-state-toggle').checked) { + let enable_autostate_worker = new Worker( + `data:text/javascript, + let s = require('child_process').spawnSync('pkexec', ['rm', '-f', '/blend/states/.disable_states']).status + if (s === 0) { + postMessage('success') + } else { + postMessage('failure') + } + ` + ) + enable_autostate_worker.onmessage = e => { + if (e.data == 'success') { + document.getElementById('automatic-state-toggle').checked = false + } else { + document.getElementById('automatic-state-toggle').checked = true + } + } + } else { + let disable_autostate_worker = new Worker( + `data:text/javascript, + let s = require('child_process').spawnSync('pkexec', ['blend-system', 'toggle-states']).status + if (s === 0) { + postMessage('success') + } else { + postMessage('failure') + } + ` + ) + disable_autostate_worker.onmessage = e => { + if (e.data == 'success') { + document.getElementById('automatic-state-toggle').checked = true + } else { + document.getElementById('automatic-state-toggle').checked = false + } + } + } +}); \ No newline at end of file diff --git a/blend-settings/src/pages/containers.html b/blend-settings/src/pages/containers.html new file mode 100644 index 0000000..95ebfc7 --- /dev/null +++ b/blend-settings/src/pages/containers.html @@ -0,0 +1,49 @@ +
+
+ Containers +

You can install any app from any of the supported distributions (Arch, Fedora, and Ubuntu). Any apps you install in them will appear as regular applications on your system, and so will any binaries. In case a binary is common between two containers, the binary from the most recently created container will be exported. You can override this by rearranging (dragging) the containers below to select the priority that should be assigned to each container.

+
+
+
+
+ Loading list of containers. +

You'll find a list of all the containers here.

+
+
+
+
+
+
+ +
+
+ Create new container +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+
+ + + + + + \ No newline at end of file diff --git a/blend-settings/src/pages/overlay.html b/blend-settings/src/pages/overlay.html new file mode 100644 index 0000000..52aab29 --- /dev/null +++ b/blend-settings/src/pages/overlay.html @@ -0,0 +1,64 @@ +
+
+ System Settings +

+
+
+
+
+ Disable automatic state creation +

blendOS creates copies of apps and config every 6 hours (and keeps the + last two).

+
+
+
+ +
+
+
+
+
+
+
+ Save current state +

Create a copy of the current system state, including apps and config.

+
+
+
+ +
+
+
+
+
+
+
+ Rollback +

Rollback to the most recent state on the next boot. (note: this is irreversible) +

+
+
+
+ +
+
+
+
+
+
+
+ +
+
+

+ You can install packages just like you would on a regular Arch system, or install them in containers. +

+
+
+ + + + + + \ No newline at end of file diff --git a/blend-settings/src/pages/terminal.html b/blend-settings/src/pages/terminal.html new file mode 100644 index 0000000..1b81b3c --- /dev/null +++ b/blend-settings/src/pages/terminal.html @@ -0,0 +1,110 @@ + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/blend-settings/src/preload.js b/blend-settings/src/preload.js new file mode 100644 index 0000000..e69de29 diff --git a/blend-system b/blend-system new file mode 100755 index 0000000..310e6f6 --- /dev/null +++ b/blend-system @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +# Copyright (C) 2023 Rudra Saraswat +# +# This file is part of blend. +# +# blend is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# blend is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with blend. If not, see . + + +import os, re, sys, time +import argparse +import subprocess + +__version = '2.0.0' + +### Colors +class colors: + reset = '\033[0m' + bold = '\033[01m' + disable = '\033[02m' + underline = '\033[04m' + reverse = '\033[07m' + strikethrough = '\033[09m' + invisible = '\033[08m' + + class fg: + black = '\033[30m' + red = '\033[31m' + green = '\033[32m' + orange = '\033[33m' + blue = '\033[34m' + purple = '\033[35m' + cyan = '\033[36m' + lightgrey = '\033[37m' + darkgrey = '\033[90m' + lightred = '\033[91m' + lightgreen = '\033[92m' + yellow = '\033[93m' + lightblue = '\033[94m' + pink = '\033[95m' + lightcyan = '\033[96m' + + class bg: + black = '\033[40m' + red = '\033[41m' + green = '\033[42m' + orange = '\033[43m' + blue = '\033[44m' + purple = '\033[45m' + cyan = '\033[46m' + lightgrey = '\033[47m' + +### END + +### Helper functions + +def info(msg): + print (colors.bold + colors.fg.cyan + '>> i: ' + colors.reset + colors.bold + msg + colors.reset) + +def error(err): + print (colors.bold + colors.fg.red + '>> e: ' + colors.reset + colors.bold + err + colors.reset) + +def load_prev_state(): + subprocess.call(['rm', '-rf', '/blend/overlay/current'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.call(['mkdir', '-p', '/blend/overlay/current/usr'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.call(['tar', '-xvpzf', f'/blend/states/state{current_state()}.tar.gz', '-C', '/blend/overlay/current/usr', '--numeric-owner'], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.call(['rm', '-f', f'/blend/states/state{current_state()}.tar.gz'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + +### END + +def current_state(): + _state = -1 + for s in os.listdir('/blend/states'): + if re.match(r'^state([0-9]+)\.tar\.gz$', s): + if int(s[5:-7]) > _state: + _state = int(s[5:-7]) + return _state + +def load_overlay(): + if os.path.isfile('/blend/states/.load_prev_state') and os.path.isfile(f'/blend/states/state{current_state()}.tar.gz'): + load_prev_state() + os.remove('/blend/states/.load_prev_state') + subprocess.call(['mkdir', '-p', '/blend/overlay/current/usr'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.call(['rm', '-rf', '/blend/overlay/workdir'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.call(['mkdir', '-p', '/blend/overlay/workdir'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.call(['touch', '/blend/overlay/current/usr/.blend_overlay'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.call(['chattr', '+i', '/blend/overlay/current/usr/.blend_overlay'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.call(['mount', '-t', 'overlay', 'overlay', '-o', 'rw,lowerdir=/usr,upperdir=/blend/overlay/current/usr,workdir=/blend/overlay/workdir', + '/usr', '-o', 'index=off'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + info('mounted overlay') + +def save_state(): + subprocess.call(['mkdir', '-p', '/blend/states'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + state = current_state() + 1 + + subprocess.call(r"find /blend/states/ -type f -not -name 'state" + str(state - 1) + ".tar.gz' -print0 | xargs -0 -I {} rm {}", stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=True) + subprocess.call(['tar', '-C', '/blend/overlay/current/usr', '-cpzf', f'/blend/states/state{state}.tar.gz', '.'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + info(f'saved state {state}') + +def autosave_state(): + while True: + if not os.path.isfile('/blend/states/.disable_states'): + save_state() + time.sleep(6*60*60) # XXX: make this configurable + +def toggle_states(): + if os.path.isfile('/blend/states/.disable_states'): + os.remove('/blend/states/.disable_states') + info('enabled saving states automatically (every 6 hours; this will be configurable in future releases)') + else: + subprocess.call(['touch', '/blend/states/.disable_states'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + info('disabled saving states automatically') + +def rollback(): + if current_state() == -1: + error('no states present') + exit(1) + subprocess.call(['touch', '/blend/states/.load_prev_state'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + info(f'will rollback to the previous state on the next boot') + +description = f''' +{colors.bold}{colors.fg.purple}Usage:{colors.reset} + blend-system [command] [options] [arguments] + +{colors.bold}{colors.fg.purple}Version:{colors.reset} {__version}{colors.bold} + +{colors.bold}{colors.fg.purple}available commands{colors.reset}: + {colors.bold}help{colors.reset} Show this help message and exit. + {colors.bold}version{colors.reset} Show version information and exit. + {colors.bold}load-overlay{colors.reset} Load the current overlay. + {colors.bold}save-state{colors.reset} Save the current state (backup). + {colors.bold}toggle-states{colors.reset} Enable/disable automatic state creation (you can still manually save states). + {colors.bold}rollback{colors.reset} Rollback to previous state. + +{colors.bold}{colors.fg.purple}options for commands{colors.reset}: + {colors.bold}-v, --version{colors.reset} show version information and exit +''' + +epilog = f''' +{colors.bold}Made with {colors.fg.red}\u2764{colors.reset}{colors.bold} by Rudra Saraswat.{colors.reset} +''' + +parser = argparse.ArgumentParser(description=description, usage=argparse.SUPPRESS, + epilog=epilog, formatter_class=argparse.RawTextHelpFormatter) +command_map = { 'help': 'help', + 'version': 'version', + 'load-overlay': load_overlay, + 'save-state': save_state, + 'toggle-states': toggle_states, + 'autosave-state': autosave_state, + 'rollback': rollback } +parser.add_argument('command', choices=command_map.keys(), help=argparse.SUPPRESS) +parser.add_argument('-v', '--version', action='version', version=f'%(prog)s {__version}', help=argparse.SUPPRESS) + +if len(sys.argv) == 1: + parser.print_help() + exit() + +if os.geteuid() != 0: + error('requires root') + exit(1) + +args = parser.parse_intermixed_args() + +command = command_map[args.command] + +if command == 'help': + parser.print_help() +elif command == 'version': + parser.parse_args(['--version']) +else: + command() diff --git a/blend-system.service b/blend-system.service new file mode 100644 index 0000000..51f3b06 --- /dev/null +++ b/blend-system.service @@ -0,0 +1,10 @@ +[Unit] +Description=Save system state + +[Service] +Type=simple +ExecStart=/usr/bin/blend-system autosave-state +User=root + +[Install] +WantedBy=multi-user.target diff --git a/blend.hook b/blend.hook new file mode 100644 index 0000000..7887320 --- /dev/null +++ b/blend.hook @@ -0,0 +1,22 @@ +#!/bin/bash + +run_latehook() { + if [[ -f /new_root/blend/states/.load_prev_state ]] && compgen -G "/new_root/blend/states/state+([0-9]).tar.gz" >/dev/null; then + rm -rf /new_root/blend/overlay/current + mkdir -p /new_root/blend/overlay/current/usr + c=0 + for i in $(compgen -G "/new_root/blend/states/state*.tar.gz"); do + n="${i:19:-7}" + if [[ "$n" -gt "$c" ]]; then + c="$n" + fi + done + tar -xvpzf "/new_root/blend/states/state${c}.tar.gz" -C "/new_root/blend/overlay/current/usr" --numeric-owner &>/dev/null + rm -f "/new_root/blend/states/state${c}.tar.gz" "/new_root/blend/states/.load_prev_state" + fi + + mkdir -p /new_root/blend/overlay/current/usr /new_root/usr + rm -rf /new_root/blend/overlay/workdir + mkdir -p /new_root/blend/overlay/workdir + mount -t overlay overlay -o 'lowerdir=/new_root/usr,upperdir=/new_root/blend/overlay/current/usr,workdir=/new_root/blend/overlay/workdir' /new_root/usr -o index=off +} diff --git a/blend.install b/blend.install new file mode 100644 index 0000000..2eb9e15 --- /dev/null +++ b/blend.install @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-3.0 + +build() { + add_module overlay + add_binary bash + add_binary tar + add_runscript +} + +help() { + cat <. + +[ -f /run/.containerenv ] || echo 'not running in a blend container' +[ -f /run/.containerenv ] || exit 1 + +args="" + +if [ ! -t 1 ] || [ "$(basename $0)" == xdg-open ] || [ "$(basename $0)" == gio ]; then + args="${args} --no-pty" +fi + +if [ "$(basename $0)" == host-blend ]; then + if [ "$#" -ne 1 ]; then + host-spawn $args sh; exit $? + else + host-spawn $args "$@"; exit $? + fi +else + host-spawn $args "$(basename $0)" "$@"; exit $? +fi diff --git a/init-blend b/init-blend new file mode 100755 index 0000000..b610b20 --- /dev/null +++ b/init-blend @@ -0,0 +1,257 @@ +#!/usr/bin/env bash +# Copyright (C) 2023 Rudra Saraswat +# +# This file is part of blend. +# +# blend is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# blend is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with blend. If not, see . + +if [ ! -f '/run/.containerenv' ]; then + echo 'not running in container' + exit 1 +fi + +while true; do + case $1 in + --uid) + if [ -n "$2" ]; then + _cuid="$2" + shift + shift + fi + ;; + --group) + if [ -n "$2" ]; then + _cgid="$2" + shift + shift + fi + ;; + --username) + if [ -n "$2" ]; then + _uname="$2" + shift + shift + fi + ;; + --home) + if [ -n "$2" ]; then + _uhome="$2" + shift + shift + fi + ;; + -*) + exit 1 + ;; + *) + break + ;; + esac +done + +cat << 'EOF' + + + ▄▄▄▄ ██▓ ▓█████ ███▄ █ ▓█████▄ +▓█████▄ ▓██▒ ▓█ ▀ ██ ▀█ █ ▒██▀ ██▌ +▒██▒ ▄██▒██░ ▒███ ▓██ ▀█ ██▒░██ █▌ +▒██░█▀ ▒██░ ▒▓█ ▄ ▓██▒ ▐▌██▒░▓█▄ ▌ +░▓█ ▀█▓░██████▒░▒████▒▒██░ ▓██░░▒████▓ +░▒▓███▀▒░ ▒░▓ ░░░ ▒░ ░░ ▒░ ▒ ▒ ▒▒▓ ▒ +▒░▒ ░ ░ ░ ▒ ░ ░ ░ ░░ ░░ ░ ▒░ ░ ▒ ▒ + ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ + ░ ░ ░ ░ ░ ░ ░ + ░ ░ + +EOF + +echo +echo 'Starting blend... (this may take a few minutes)' +echo + +bmount() { + ! [[ -d "$1" ]] && ! [[ -f "$1" ]] && return 0 # check if source dir exists + ! [[ -e "$2" ]] && findmnt "$2" &>/dev/null && umount "$2" # unmount target dir if a mount + + [[ -d "$1" ]] && mkdir -p "$2" # create target dir if source is a dir + [[ -f "$1" ]] && touch "$2" # create target file if source is a file + + mountflags="rslave" + + ! [[ -z "$3" ]] && mountflags="$3" + + mount --rbind -o "$mountflags" "$1" "$2" &>/dev/null +} + +if [[ ! -f '/.init_blend.lock' ]]; then + +### + +if command -v apt-get &>/dev/null; then + apt-get update &>/dev/null + DEBIAN_FRONTEND=noninteractive apt-get -y install bash bc curl less wget apt-utils apt-transport-https dialog \ + diffutils findutils gnupg2 sudo time util-linux libnss-myhostname \ + libvte-2.9[0-9]-common libvte-common lsof ncurses-base passwd \ + pinentry-curses libegl1-mesa libgl1-mesa-glx libvulkan1 mesa-vulkan-drivers &>/dev/null +elif command -v pacman &>/dev/null; then + pacman --noconfirm -Syyu &>/dev/null + pacman --noconfirm -Sy bash bc curl wget diffutils findutils gnupg sudo time util-linux vte-common lsof ncurses pinentry \ + mesa opengl-driver vulkan-intel vulkan-radeon &>/dev/null +elif command -v dnf &>/dev/null; then + dnf install -y --allowerasing bash bc curl wget diffutils findutils dnf-plugins-core gnupg2 less lsof passwd pinentry \ + procps-ng vte-profile ncurses util-linux sudo time shadow-utils vulkan mesa-vulkan-drivers \ + mesa-dri-drivers &>/dev/null + +fi + +mkdir -p /usr/local/bin +wget -O /usr/local/bin/host-spawn "https://github.com/1player/host-spawn/releases/latest/download/host-spawn-$(uname -m)" &>/dev/null +chmod 755 /usr/local/bin/host-spawn + +fi + +### + +for i in /var/log/journal /var/lib/systemd/coredump /var/lib/flatpak; do + bmount "/run/host/${i}" "$i" ro +done + +for i in /etc/host.conf /run/media /media /mnt /var/mnt \ + /run/libvirt /etc/machine-id /run/netconfig/ /run/udev \ + /run/systemd/journal /run/systemd/seats /run/systemd/sessions \ + /run/systemd/users /run/systemd/resolve/ /srv /var/lib/libvirt; do + bmount "/run/host/${i}" "$i" rw +done + +init_ro_mounts=" + /run/systemd/journal + /var/log/journal + /run/systemd/resolve + /run/systemd/seats + /run/systemd/sessions + /run/systemd/users + /var/lib/systemd/coredump + /etc/localtime" + +### Section START https://github.com/89luca89/distrobox/blob/main/distrobox-init#L772 +host_sockets="$(find /run/host/run -name 'user' \ + -prune -o -path /run/host/run/media \ + -prune -o -name 'nscd' \ + -prune -o -name 'bees' \ + -prune -o -name 'system_bus_socket' \ + -prune -o -type s -print \ + 2> /dev/null || :)" +### Section END + +for i in ${host_sockets}; do + container_socket="$(echo -n "$i" | sed 's/\/run\/host//g')" + if [ ! -S "${container_socket}" ] && [ ! -L "${container_socket}" ]; then + rm -f "${container_socket}" + mkdir -p "$(dirname "${container_socket}")" + ln -s "$i" "${container_socket}" + fi +done + +bmount "/run/host/usr/share/themes" "/usr/local/share/themes" ro +bmount "/run/host/usr/share/icons" "/usr/local/share/icons" ro +bmount "/run/host/usr/share/fonts" "/usr/local/share/fonts" ro + +bmount "/usr/bin/host-blend" "/usr/bin/blend" ro + +# sudo touch /.init_blend.lock + +if [[ ! -f '/.init_blend.lock' ]]; then + +### Section START (based on https://github.com/89luca89/distrobox/blob/main/distrobox-init#L816) + +if [ -d "/usr/lib/rpm/" ]; then + mkdir -p /usr/lib/rpm/macros.d + net_mounts="" + for net_mount in \ + ${HOST_MOUNTS_RO} ${HOST_MOUNTS} \ + '/dev' '/proc' '/sys' '/tmp' \ + '/etc/host.conf' '/etc/hosts' '/etc/resolv.conf' '/etc/localtime' \ + '/usr/share/zoneinfo'; do + + net_mounts="${net_mount}:${net_mounts}" + + done + net_mounts=${net_mounts%?} + echo "%_netsharedpath ${net_mounts}" > /usr/lib/rpm/macros.d/macros.blend +elif [ -d "/etc/dpkg/" ]; then + mkdir -p /etc/dpkg/dpkg.cfg.d + echo -n > /etc/dpkg/dpkg.cfg.d/00_blend + for net_mount in ${HOST_MOUNTS_RO} ${HOST_MOUNTS} '/etc/hosts' '/etc/resolv.conf' '/etc/localtime'; do + printf "path-exclude %s/*\n" "${net_mount}" >> /etc/dpkg/dpkg.cfg.d/00_blend + done +### Section END +elif [ -d "/usr/share/libalpm/scripts" ]; then + echo "#!/bin/sh" > /usr/share/libalpm/scripts/00_blend_pre_hook.sh + echo "#!/bin/sh" > /usr/share/libalpm/scripts/01_blend_post_hook.sh + echo "#!/bin/sh" > /usr/share/libalpm/scripts/02_blend_post_hook.sh + + for net_mount in ${HOST_MOUNTS_RO}; do + echo "findmnt ${net_mount} &>/dev/null && umount ${net_mount} || :" >> /usr/share/libalpm/scripts/00_blend_pre_hook.sh + echo "test -e /run/host/${net_mount} && mount --rbind -o ro /run/host/${net_mount} ${net_mount} || :" >> /usr/share/libalpm/scripts/02_blend_post_hook.sh + done + + echo -e '#!/bin/sh\necho -e "#!/bin/sh\nexit 0" > /usr/share/libalpm/scripts/systemd-hook' >/usr/share/libalpm/scripts/01_blend_post_hook.sh + + chmod 755 /usr/share/libalpm/scripts/*blend*.sh + + for p in 00_blend_pre_hook 01_blend_post_hook.sh 02_blend_post_hook; do + when=PostTransaction + + [[ -z "${p##*pre*}" ]] && when=PreTransaction +cat << EOF > "/usr/share/libalpm/hooks/${p}.hook" +[Trigger] +Operation = Install +Operation = Upgrade +Type = Package +Target = * +[Action] +Description = blend ${p} +When = ${when} +Exec = /usr/share/libalpm/scripts/${p}.sh +EOF + done +fi + +mkdir -p /etc/sudoers.d +if ! grep -q 'Defaults !fqdn' /etc/sudoers.d/sudoers &>/dev/null; then + printf "Defaults !fqdn\n" >> /etc/sudoers.d/sudoers +fi +if ! grep -q "\"${_uname}\" ALL = (root) NOPASSWD:ALL" /etc/sudoers.d/sudoers &>/dev/null; then + printf "\"%s\" ALL = (root) NOPASSWD:ALL\n" "$_uname" >> /etc/sudoers.d/sudoers +fi +if ! grep -q "^${_uname}:" /etc/group; then + if ! groupadd --force --gid "$_cgid" "$_uname"; then + printf "%s:x:%s:" "$_uname" "$_cgid" >> /etc/group + fi +fi +useradd --uid "$_cuid" --gid "$_cgid" --shell "/bin/bash" --no-create-home --home "$_uhome" "$_uname" &>/dev/null + +fi + +touch /.init_blend.lock + +echo +echo "Completed container setup." + +while true; do + for i in /etc/hosts /etc/localtime /etc/resolv.conf; do + cp "/run/host/${i}" / &>/dev/null || : + done + sleep 5 +done \ No newline at end of file diff --git a/pkgmanagers/apt b/pkgmanagers/apt deleted file mode 100755 index 1af4b30..0000000 --- a/pkgmanagers/apt +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -BLEND_COMMAND="sudo apt $@" blend enter ubuntu-22.10 --distro ubuntu-22.10 -ret=$? - -exit $ret diff --git a/pkgmanagers/apt-get b/pkgmanagers/apt-get deleted file mode 100755 index 5f757f6..0000000 --- a/pkgmanagers/apt-get +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -BLEND_COMMAND="sudo apt-get $@" blend enter ubuntu-22.10 --distro ubuntu-22.10 -ret=$? - -exit $ret diff --git a/pkgmanagers/dnf b/pkgmanagers/dnf deleted file mode 100755 index 4e5f766..0000000 --- a/pkgmanagers/dnf +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -BLEND_COMMAND="sudo dnf $@" blend enter fedora-rawhide --distro fedora-rawhide -ret=$? - -exit $ret diff --git a/pkgmanagers/pacman b/pkgmanagers/pacman deleted file mode 100755 index 610be8d..0000000 --- a/pkgmanagers/pacman +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -[[ -z "$SUDO_USER" ]] || { - [[ "$SUDO_USER" == root ]] || { SUDO_USER= sudo -u "$SUDO_USER" "$0" "$@"; exit $?; } -} - -BLEND_COMMAND="sudo pacman $@" blend enter arch -ret=$? - -exit $ret diff --git a/pkgmanagers/yay b/pkgmanagers/yay deleted file mode 100755 index d53807d..0000000 --- a/pkgmanagers/yay +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -BLEND_COMMAND="yay $@" blend enter arch -ret=$? - -exit $ret diff --git a/pkgmanagers/yum b/pkgmanagers/yum deleted file mode 100755 index 120463c..0000000 --- a/pkgmanagers/yum +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -BLEND_COMMAND="sudo yum $@" blend enter fedora-rawhide --distro fedora-rawhide -ret=$? - -exit $ret From 958a3244db24e1cf4213f082a0761a82d0d03e5d Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sat, 11 Feb 2023 18:11:35 +0530 Subject: [PATCH 026/121] add electron-builder --- blend-settings/package.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/blend-settings/package.json b/blend-settings/package.json index 0f547c7..1d9642f 100644 --- a/blend-settings/package.json +++ b/blend-settings/package.json @@ -14,6 +14,7 @@ "dependencies": { "@electron/remote": "^2.0.9", "@types/jquery": "^3.5.16", + "electron-builder": "^23.6.0", "jquery": "^3.6.3", "js-yaml": "^4.1.0", "node-pty": "github:daniel-brenot/node-pty#rust-port", @@ -24,5 +25,15 @@ }, "devDependencies": { "electron": "^23.0.0" + }, + "build": { + "appId": "org.blend.settings", + "productName": "blendOS Settings", + "asar": true, + "linux": { + "target": ["deb"], + "category": "System", + "maintainer": "Rudra Saraswat" + } } } From 04f4945863ecfbb618eb40a4270830475b314b05 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sat, 11 Feb 2023 18:11:55 +0530 Subject: [PATCH 027/121] update version number in blend-settings --- blend-settings/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blend-settings/package.json b/blend-settings/package.json index 1d9642f..6dd82ae 100644 --- a/blend-settings/package.json +++ b/blend-settings/package.json @@ -1,6 +1,6 @@ { "name": "blend-settings", - "version": "1.0.0", + "version": "2.0.0", "description": "A settings app for blendOS", "main": "main.js", "homepage": "https://blendos.org", From e830548c5677984d79f2e7f45fdbfd9f46515ad5 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sat, 11 Feb 2023 18:32:49 +0530 Subject: [PATCH 028/121] update icons --- .gitignore | 1 + blend-settings/.gitignore | 4 ---- blend-settings/build/icon.png | Bin 16225 -> 0 bytes blend-settings/package.json | 8 +++++--- blend-settings/static/icon.png | Bin 0 -> 46635 bytes 5 files changed, 6 insertions(+), 7 deletions(-) delete mode 100644 blend-settings/.gitignore delete mode 100644 blend-settings/build/icon.png create mode 100644 blend-settings/static/icon.png diff --git a/.gitignore b/.gitignore index 4fe686f..29cb41c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /blend-settings/node_modules /blend-settings/package-lock.json +/blend-settings/build/ \ No newline at end of file diff --git a/blend-settings/.gitignore b/blend-settings/.gitignore deleted file mode 100644 index abc6562..0000000 --- a/blend-settings/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -dist/ -node_modules/ -package-lock.json -web-server/data/applist.json \ No newline at end of file diff --git a/blend-settings/build/icon.png b/blend-settings/build/icon.png deleted file mode 100644 index a2fce7522f00900bfb8feefa7df78265ce151e6e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16225 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≈nSNdTuibcc-M za1TkY{qpC0p%W{NT|t*zK8zl1AX~dLe$ZQ`rQd?@T${~EQ{M44-Wot?`*;MA*8VcZ z9hj88{n0UVIvYvuCX?olb+=OxPueN@SGcZYIZ$v;q~k|4>Tby}XiXPR@~SJmg=FRi}sox_tA zS&urNY0`y|2Ev$aSuUMG7n@IB>)YgZt<(cfBi@pB`u21 zx9!>dIc?f;p4WAE%$4^F$=JpF7;>QTjnn$}7DgJF4jef)%b@pca-o6yANk9-KsnIt zf-6nVf#IF7Gn8o#L3keryWajx9RZQ78O1pT*(F7?0EX6GldBDEn%91*AnnB)A~^B{ zCrIscQ939!PMWB+^P}eQbAbL!tct@0rjs#)^YOIAkFPNm9qur>)-&z106487W)J5v zqFf?F5Nix9>iYERIXw}@&#<$1;eoz^EJHUn69GsU=s8kowEin|x_KA`Tw`sfego4L z&5!?%76HL89#3btYhpki6cYnD&3VYa20mZXBByW8AZ$ZLKC#}*5x}>~QWATv_%Zk9 zXh^|4g)9;;ki?i~UeIQKCJ+n@5zK%l#zjdsuMSC)f~%5nVzNoq%teS?N;6E# zoFRA6Uu|l2DnjCcq@_;}qz!%SeWBLH9T;B_1Mj&u6wFC`{N8`3k`Tj4q=E%m_Z~zu zh!=+Xs;+8e(h(B3vM^1ylihmA#|`=11|J>od_3)v;s}_`EZ~x|$v5`tx(z=6BN&wAA$I=ueTZ-sp_}@O+z6?I zlmB0HKEd;8z55gs6-2EfU==CfZxo1Y&jcD@GSildrO4&JTm@#e9-sm z1N6by#WlYdvPrC4rk*%cyS6oMyz@hp6x_{|a+t7@4FhHGEwm-8NtP|c4EdTjeWAwb z9W;VI?2e%p$>XhoAM;JtYxxOf?3&X~+w*vbt!W^&470Y=zxj0!fY8rReB}5WCY?G9 z5aN{$ARd5o4$@ygkbd*g;DuVx-EflX0us4zSm)j0wW9*Ad(vh<4=uWt`H6y≥Yng1U@)Z^z&G*F# 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ξdjwr*-^dlbFs#$B8)7-)#%COk3yG^5yOPEdH) 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 E2VT4CFaQ7m literal 0 HcmV?d00001 From 0024d6692b25fd14cc97add5ed80bcda959d3904 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sat, 11 Feb 2023 18:33:37 +0530 Subject: [PATCH 029/121] add icon to package.json --- blend-settings/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/blend-settings/package.json b/blend-settings/package.json index 5122b1c..a3d5a7c 100644 --- a/blend-settings/package.json +++ b/blend-settings/package.json @@ -35,6 +35,7 @@ "linux": { "target": ["tar.gz"], "category": "System", + "icon": "icons/png", "maintainer": "Rudra Saraswat" } } From 03939a43cb534936c71accd5cace06785f0bf585 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sat, 11 Feb 2023 20:36:47 +0530 Subject: [PATCH 030/121] update to 2.0.0 --- PKGBUILD | 69 +++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 226102f..7ba7e21 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,21 +1,64 @@ # Maintainer: Rudra Saraswat -pkgname=blend -pkgver=1.0.4 -pkgrel=2 +pkgbase=blend +pkgname=(blend blend-settings) +pkgver=2.0.0 +pkgrel=1 pkgdesc='A package manager for blendOS' url='https://github.com/blend-os/blend' -source=("git+https://github.com/blend-os/blend.git") -sha256sums=('SKIP') +source=("git+https://github.com/blend-os/blend.git" + "blend-settings.desktop" + "blend-settings") +sha256sums=('SKIP' + 'a605d24d2fa7384b45a94105143db216db1ffc0bdfc7f6eec758ef2026e61e54' + '73cb7c39190d36f233b8dfbc3e3e6737d56e61e90881ad95f09e5ae1f9b405a8') arch=('x86_64') +makedepends=(electron) license=('GPL-3.0-or-later') -depends=('python3' 'distrobox' 'podman') -package() { - cd blend - - mkdir -p "${pkgdir}"/{usr/{bin,share/bash-completion/completions},blend} - cp blend "${pkgdir}/usr/bin" - cp completions/blend "${pkgdir}/usr/share/bash-completion/completions" - cp -r pkgmanagers "${pkgdir}/blend" +build() { + cd "${srcdir}"/blend/blend-settings + npm install + export NODE_ENV=production + npm run icons + npm run pack -- -c.electronDist=/usr/lib/electron -c.electronVersion=22 --publish never +} + +package_blend() { + depends=('python3' 'podman' 'python-pexpect' 'bash') + cd "${srcdir}"/blend + mkdir -p "${pkgdir}"/usr/bin "${pkgdir}"/usr/lib/systemd/system "${pkgdir}"/usr/lib/systemd/user "${pkgdir}"/usr/lib/initcpio/install "${pkgdir}"/usr/lib/initcpio/hooks + cp blend init-blend host-blend blend-system blend-files "${pkgdir}"/usr/bin + cp blend-system.service "${pkgdir}"/usr/lib/systemd/system + cp blend-files.service "${pkgdir}"/usr/lib/systemd/user + cp blend.hook "${pkgdir}"/usr/lib/initcpio/hooks/blend + cp blend.install "${pkgdir}"/usr/lib/initcpio/install/blend +} + +package_blend-settings() { + # https://aur.archlinux.org/cgit/aur.git/tree/PKGBUILD?h=kuro was really helpful + + depends=('electron') + cd "${srcdir}"/blend/blend-settings + local _arch + case $CARCH in + i686) + _arch=linux-ia32-unpacked + ;; + x86_64) + _arch=linux-unpacked + ;; + *) + _arch=linux-$CARCH-unpacked + ;; + esac + install -Dm644 "dist/${_arch}/resources/app.asar" "$pkgdir/usr/lib/blend-settings/blend-settings.asar" + for icon_size in 16 24 32 48 64 128 256 512; do + install -Dm644 \ + "build/icons/png/${icon_size}x${icon_size}.png" \ + "${pkgdir}/usr/share/icons/hicolor/${icon_size}x${icon_size}/apps/blend-settings.png" + done + install -Dm644 -t "${pkgdir}/usr/share/applications" "../../blend-settings.desktop" + install -Dm755 -t "${pkgdir}/usr/bin" "../../blend-settings" + install -Dm644 "../LICENSE.md" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" } From f82a107819d6307df2508a4973653e810ef94f1c Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sat, 11 Feb 2023 20:48:00 +0530 Subject: [PATCH 031/121] update gitignore --- .gitignore | 2 ++ blend-settings | 2 ++ blend-settings.desktop | 9 +++++++++ 3 files changed, 13 insertions(+) create mode 100755 blend-settings create mode 100644 blend-settings.desktop diff --git a/.gitignore b/.gitignore index c6a9b95..8fee7f7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ !.gitignore !PKGBUILD +!blend-settings +!blend-settings.desktop !.SRCINFO diff --git a/blend-settings b/blend-settings new file mode 100755 index 0000000..f297284 --- /dev/null +++ b/blend-settings @@ -0,0 +1,2 @@ +#!/bin/sh +exec electron /usr/lib/blend-settings/blend-settings.asar "$@" diff --git a/blend-settings.desktop b/blend-settings.desktop new file mode 100644 index 0000000..8137fd9 --- /dev/null +++ b/blend-settings.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=blendOS Settings +Exec=blend-settings %U +Terminal=false +Type=Application +Icon=blend-settings +StartupWMClass=blend-settings +Comment=Settings for blendOS. +Categories=System; From 62b07424e9df8ea461a121975812c72f67326dbb Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sat, 11 Feb 2023 21:48:49 +0530 Subject: [PATCH 032/121] update deps --- PKGBUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PKGBUILD b/PKGBUILD index 7ba7e21..e306d36 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -25,7 +25,7 @@ build() { } package_blend() { - depends=('python3' 'podman' 'python-pexpect' 'bash') + depends=('python3' 'podman' 'python-pexpect' 'blend-settings' 'bash') cd "${srcdir}"/blend mkdir -p "${pkgdir}"/usr/bin "${pkgdir}"/usr/lib/systemd/system "${pkgdir}"/usr/lib/systemd/user "${pkgdir}"/usr/lib/initcpio/install "${pkgdir}"/usr/lib/initcpio/hooks cp blend init-blend host-blend blend-system blend-files "${pkgdir}"/usr/bin From bf55407848a514ec305339cc2bd1f5d94274158c Mon Sep 17 00:00:00 2001 From: Mark Wagie Date: Mon, 27 Mar 2023 16:24:18 -0600 Subject: [PATCH 033/121] Improve PKGBUILD - This is pulling straight from the master branch, it cannot have a static pkgver. Create git tags or use a static commit if you want to release '2.0.0' or whatever - Use a local npm cache as not to pollute a users home directory - Run `npm install` in the prepare function for offline building - Use a variable for the Electron version for easier updating - Add separate pkgdesc for blend-settings-git - Use `install -DmXXX`, no need for `mkdir -p` + `cp` --- PKGBUILD | 103 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 67 insertions(+), 36 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index e306d36..6af26cf 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,47 +1,75 @@ # Maintainer: Rudra Saraswat -pkgbase=blend -pkgname=(blend blend-settings) -pkgver=2.0.0 +pkgbase=blend-git +pkgname=('blend-git' 'blend-settings-git') +pkgver=r27.0024d66 pkgrel=1 -pkgdesc='A package manager for blendOS' -url='https://github.com/blend-os/blend' -source=("git+https://github.com/blend-os/blend.git" - "blend-settings.desktop" - "blend-settings") +_electronversion=22 +pkgdesc="A package manager for blendOS" +arch=('x86_64' 'i686') +url="https://github.com/blend-os/blend" +license=('GPL3') +makedepends=("electron${_electronversion}" 'git' 'npm') +source=('git+https://github.com/blend-os/blend.git' + 'blend-settings.desktop' + 'blend-settings') sha256sums=('SKIP' 'a605d24d2fa7384b45a94105143db216db1ffc0bdfc7f6eec758ef2026e61e54' '73cb7c39190d36f233b8dfbc3e3e6737d56e61e90881ad95f09e5ae1f9b405a8') -arch=('x86_64') -makedepends=(electron) -license=('GPL-3.0-or-later') + +pkgver() { + cd "${srcdir}/${pkgbase%-git}" + printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" +} + +prepare() { + cd "${srcdir}/${pkgbase%-git}/${pkgbase%-git}-settings" + npm config set cache "${srcdir}/npm-cache" + npm install +} build() { - cd "${srcdir}"/blend/blend-settings - npm install + cd "${srcdir}/${pkgbase%-git}/${pkgbase%-git}-settings" + npm config set cache "${srcdir}/npm-cache" export NODE_ENV=production + electronDist="/usr/lib/electron${_electronversion}" + electronVer="$(sed s/^v// /usr/lib/electron${_electronversion}/version)" npm run icons - npm run pack -- -c.electronDist=/usr/lib/electron -c.electronVersion=22 --publish never + npm run pack -- -c.electronDist=${electronDist} \ + -c.electronVersion=${electronVer} --publish never } -package_blend() { - depends=('python3' 'podman' 'python-pexpect' 'blend-settings' 'bash') - cd "${srcdir}"/blend - mkdir -p "${pkgdir}"/usr/bin "${pkgdir}"/usr/lib/systemd/system "${pkgdir}"/usr/lib/systemd/user "${pkgdir}"/usr/lib/initcpio/install "${pkgdir}"/usr/lib/initcpio/hooks - cp blend init-blend host-blend blend-system blend-files "${pkgdir}"/usr/bin - cp blend-system.service "${pkgdir}"/usr/lib/systemd/system - cp blend-files.service "${pkgdir}"/usr/lib/systemd/user - cp blend.hook "${pkgdir}"/usr/lib/initcpio/hooks/blend - cp blend.install "${pkgdir}"/usr/lib/initcpio/install/blend +package_blend-git() { + depends=('bash' 'blend-settings' 'podman' 'python' 'python-pexpect') + provides=("${pkgname%-git}") + conflicts=("${pkgname%-git}") + + cd "${srcdir}/${pkgbase%-git}" + install -Dm755 \ + "${pkgname%-git}" \ + "init-${pkgname%-git}" \ + "host-${pkgname%-git}" \ + "${pkgname%-git}-system" \ + "${pkgname%-git}-files" \ + -t "${pkgdir}"/usr/bin/ + install -Dm644 "${pkgname%-git}-system.service" -t \ + "${pkgdir}"/usr/lib/systemd/system/ + install -Dm644 "${pkgname%-git}-files.service" -t \ + "${pkgdir}"/usr/lib/systemd/user/ + install -Dm644 "${pkgname%-git}.hook" \ + "${pkgdir}/usr/lib/initcpio/hooks/${pkgname%-git}" + install -Dm644 "${pkgname%-git}.install" \ + "${pkgdir}/usr/lib/initcpio/install/${pkgname%-git}" } -package_blend-settings() { - # https://aur.archlinux.org/cgit/aur.git/tree/PKGBUILD?h=kuro was really helpful +package_blend-settings-git() { + pkgdesc="blendOS Settings" + depends=("electron${_electronversion}") + + cd "${srcdir}/${pkgbase%-git}/${pkgbase%-git}-settings" - depends=('electron') - cd "${srcdir}"/blend/blend-settings local _arch - case $CARCH in + case ${CARCH} in i686) _arch=linux-ia32-unpacked ;; @@ -49,16 +77,19 @@ package_blend-settings() { _arch=linux-unpacked ;; *) - _arch=linux-$CARCH-unpacked + _arch=linux-${CARCH}-unpacked ;; esac - install -Dm644 "dist/${_arch}/resources/app.asar" "$pkgdir/usr/lib/blend-settings/blend-settings.asar" + + install -Dm644 "dist/${_arch}/resources/app.asar" \ + "$pkgdir/usr/lib/${pkgname%-git}/${pkgname%-git}.asar" + for icon_size in 16 24 32 48 64 128 256 512; do - install -Dm644 \ - "build/icons/png/${icon_size}x${icon_size}.png" \ - "${pkgdir}/usr/share/icons/hicolor/${icon_size}x${icon_size}/apps/blend-settings.png" + install -Dm644 "build/icons/png/${icon_size}x${icon_size}.png" \ + "${pkgdir}/usr/share/icons/hicolor/${icon_size}x${icon_size}/apps/${pkgname%-git}.png" done - install -Dm644 -t "${pkgdir}/usr/share/applications" "../../blend-settings.desktop" - install -Dm755 -t "${pkgdir}/usr/bin" "../../blend-settings" - install -Dm644 "../LICENSE.md" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" + + install -Dm644 "${srcdir}/${pkgname%-git}.desktop" -t \ + "${pkgdir}"/usr/share/applications/ + install -Dm755 "${srcdir}/${pkgname%-git}" -t "${pkgdir}"/usr/bin/ } From 9f7dee08a8883e7b179828d9f66a4e8db42ff560 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Tue, 11 Apr 2023 10:24:31 +0530 Subject: [PATCH 034/121] Add initial Android app support --- .gitignore | 7 +- blend | 2 +- blend-settings/main.js | 7 +- blend-settings/package.json | 1 + blend-settings/src/index.html | 24 ++- blend-settings/src/internal/js/android.js | 171 ++++++++++++++++++ .../src/internal/js/{overlay.js => system.js} | 0 blend-settings/src/pages/android.html | 93 ++++++++++ .../src/pages/{overlay.html => system.html} | 2 +- blend-system | 13 -- blend.hook | 16 +- 11 files changed, 306 insertions(+), 30 deletions(-) create mode 100644 blend-settings/src/internal/js/android.js rename blend-settings/src/internal/js/{overlay.js => system.js} (100%) create mode 100644 blend-settings/src/pages/android.html rename blend-settings/src/pages/{overlay.html => system.html} (98%) diff --git a/.gitignore b/.gitignore index 29cb41c..a48434a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -/blend-settings/node_modules -/blend-settings/package-lock.json -/blend-settings/build/ \ No newline at end of file +/blend-settings/node_modules/ +/blend-settings/build/ +/blend-settings/dist/ +/blend-settings/package-lock.json \ No newline at end of file diff --git a/blend b/blend index abb58f5..aa5d82b 100755 --- a/blend +++ b/blend @@ -107,7 +107,7 @@ def check_container(name): _list = subprocess.run(['podman', 'ps', '-a', '--no-trunc', '--size', '--format', '{{.Names}}:{{.Mounts}}'], stdout=subprocess.PIPE).stdout.decode('utf-8').strip() for container in _list.splitlines(keepends=False): - if 'blend' in container.split(':')[1] and name.strip() == container.split(':')[0]: + if ('blend' in container.split(':')[1] or 'distrobox' in container.split(':')[1]) and name.strip() == container.split(':')[0]: return True return False diff --git a/blend-settings/main.js b/blend-settings/main.js index 03936b2..fe99cbc 100644 --- a/blend-settings/main.js +++ b/blend-settings/main.js @@ -4,6 +4,9 @@ const pty = require("node-pty"); var mainWindow, terminalWindow, ptyProcess +app.commandLine.appendSwitch('enable-transparent-visuals'); +app.disableHardwareAcceleration(); + function createWindow() { mainWindow = new BrowserWindow({ minWidth: 1000, @@ -138,7 +141,9 @@ function loadTerminalWindow(title, cmd) { app.whenReady().then(() => { app.allowRendererProcessReuse = false - createWindow() + setTimeout(() => { + createWindow(); + }, 1000); createTerminalWindow() ipcMain.on('create-term', (event, data) => { diff --git a/blend-settings/package.json b/blend-settings/package.json index a3d5a7c..075c4d5 100644 --- a/blend-settings/package.json +++ b/blend-settings/package.json @@ -9,6 +9,7 @@ "scripts": { "start": "electron .", "icons": "electron-icon-maker --input=./static/icon.png --output=./build/", + "electron-builder": "electron-builder", "pack": "electron-builder --dir", "dist": "electron-builder" }, diff --git a/blend-settings/src/index.html b/blend-settings/src/index.html index 1d73363..30506df 100644 --- a/blend-settings/src/index.html +++ b/blend-settings/src/index.html @@ -14,9 +14,11 @@
- - + + +
@@ -44,12 +46,20 @@ case 'containers': $('#webview').load("pages/containers.html"); $('#containers-button').addClass('active') - $('#overlay-button').removeClass('active') + $('#android-button').removeClass('active') + $('#system-button').removeClass('active') break; - case 'overlay': - $('#webview').load("pages/overlay.html"); + case 'android': + $('#webview').load("pages/android.html"); $('#containers-button').removeClass('active') - $('#overlay-button').addClass('active') + $('#android-button').addClass('active') + $('#system-button').removeClass('active') + break; + case 'system': + $('#webview').load("pages/system.html"); + $('#containers-button').removeClass('active') + $('#android-button').removeClass('active') + $('#system-button').addClass('active') break; } } diff --git a/blend-settings/src/internal/js/android.js b/blend-settings/src/internal/js/android.js new file mode 100644 index 0000000..a815be1 --- /dev/null +++ b/blend-settings/src/internal/js/android.js @@ -0,0 +1,171 @@ +function rollback() { + let rollback_worker = new Worker( + `data:text/javascript, + let s = require('child_process').spawnSync('pkexec', ['blend-system', 'rollback']).status + if (s === 0) { + postMessage('success') + } else { + postMessage('failure') + } + ` + ) + rollback_worker.onmessage = e => { + if (e.data == 'success') { + document.getElementById('rollback-btn').outerHTML = + '' + } else { + document.getElementById('rollback-btn').outerHTML = + '' + setTimeout(() => document.getElementById('rollback-btn').outerHTML = + '', 2000) + } + } +} + +function undo_rollback() { + let undo_rollback_worker = new Worker( + `data:text/javascript, + let s = require('child_process').spawnSync('pkexec', ['rm', '-f', '/blend/states/.load_prev_state']).status + if (s === 0) { + postMessage('success') + } else { + postMessage('failure') + } + ` + ) + undo_rollback_worker.onmessage = e => { + if (e.data == 'success') { + document.getElementById('rollback-btn').outerHTML = + '' + } else { + document.getElementById('rollback-btn').outerHTML = + '' + setTimeout(() => document.getElementById('rollback-btn').outerHTML = + '', 2000) + } + } +} + +function init_waydroid() { + document.getElementById('initialize-btn').outerHTML = + '' + let init_worker = new Worker( + `data:text/javascript, + require('child_process').spawnSync('pkexec', ['waydroid', 'init']) + require('child_process').spawn('sh', ['-c', 'waydroid session start & disown']) + setTimeout(() => { + require('child_process').spawnSync('pkexec', ['waydroid', 'shell', 'pm', 'disable', 'com.android.inputmethod.latin']) + require('child_process').spawnSync('waydroid', ['prop', 'set', 'persist.waydroid.multi_windows', 'true']) + postMessage('success') + }, 2000) + ` + ) + init_worker.onmessage = e => { + if (e.data == 'success') { + document.getElementById('init-waydroid').classList.add('d-none') + document.getElementById('waydroid-initialized-settings').classList.remove('d-none') + } + } +} + +function enable_multi_window() { + document.getElementById('multiwindow-btn').outerHTML = + '' + let multi_window_worker = new Worker( + `data:text/javascript, + require('child_process').spawn('sh', ['-c', 'waydroid session start & disown']) + setTimeout(() => { require('child_process').spawnSync('waydroid', ['prop', 'set', 'persist.waydroid.multi_windows', 'true']); require('child_process').spawn('sh', ['-c', 'waydroid session stop']); postMessage('success') }, 500) + ` + ) + multi_window_worker.onmessage = e => { + if (e.data == 'success') { + document.getElementById('multiwindow-btn').outerHTML = + '' + } + } +} + +function disable_multi_window() { + document.getElementById('multiwindow-btn').outerHTML = + '' + let multi_window_worker = new Worker( + `data:text/javascript, + require('child_process').spawn('sh', ['-c', 'waydroid session start & disown']) + setTimeout(() => { require('child_process').spawnSync('waydroid', ['prop', 'set', 'persist.waydroid.multi_windows', 'false']); require('child_process').spawn('sh', ['-c', 'waydroid session stop']); postMessage('success') }, 500) + ` + ) + multi_window_worker.onmessage = e => { + if (e.data == 'success') { + document.getElementById('multiwindow-btn').outerHTML = + '' + } + } +} + +function check_multi_window_enabled() { + let check_worker = new Worker( + `data:text/javascript, + require('child_process').spawn('sh', ['-c', 'waydroid session start & disown']) + setTimeout(() => { let val = require('child_process').spawnSync('waydroid', ['prop', 'get', 'persist.waydroid.multi_windows']).stdout; postMessage(val) }, 500) + ` + ) + check_worker.onmessage = e => { + if (new TextDecoder("utf-8").decode(e.data).trim() == 'true') { + document.getElementById('multiwindow-btn').outerHTML = + '' + } else { + document.getElementById('multiwindow-btn').outerHTML = + '' + } + } +} + +require('fs').stat('/var/lib/waydroid', (err, stat) => { + if (err == null) { + document.getElementById('waydroid-initialize-settings').classList.add('d-none') + document.getElementById('waydroid-initialized-settings').classList.remove('d-none') + } +}) + +check_state_creation() +check_rollback() + +$('#automatic-state-toggle').on('change', () => { + if (!document.getElementById('automatic-state-toggle').checked) { + let enable_autostate_worker = new Worker( + `data:text/javascript, + let s = require('child_process').spawnSync('pkexec', ['rm', '-f', '/blend/states/.disable_states']).status + if (s === 0) { + postMessage('success') + } else { + postMessage('failure') + } + ` + ) + enable_autostate_worker.onmessage = e => { + if (e.data == 'success') { + document.getElementById('automatic-state-toggle').checked = false + } else { + document.getElementById('automatic-state-toggle').checked = true + } + } + } else { + let disable_autostate_worker = new Worker( + `data:text/javascript, + let s = require('child_process').spawnSync('pkexec', ['blend-system', 'toggle-states']).status + if (s === 0) { + postMessage('success') + } else { + postMessage('failure') + } + ` + ) + disable_autostate_worker.onmessage = e => { + if (e.data == 'success') { + document.getElementById('automatic-state-toggle').checked = true + } else { + document.getElementById('automatic-state-toggle').checked = false + } + } + } +}); \ No newline at end of file diff --git a/blend-settings/src/internal/js/overlay.js b/blend-settings/src/internal/js/system.js similarity index 100% rename from blend-settings/src/internal/js/overlay.js rename to blend-settings/src/internal/js/system.js diff --git a/blend-settings/src/pages/android.html b/blend-settings/src/pages/android.html new file mode 100644 index 0000000..da4b9d4 --- /dev/null +++ b/blend-settings/src/pages/android.html @@ -0,0 +1,93 @@ +
+
+
+
+
+
+
+ Initialize Android App Support +

Initialize WayDroid to be able to run Android apps.

+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ App Lounge (/e/) +

An installable catalogue of FOSS Android applications.

+
+
+
+ +
+
+
+
+
+ Install a store +
+
+
+
+
+ App Lounge (/e/) +

An installable catalogue of FOSS Android applications.

+
+
+
+ +
+
+
+
+
+
+
+ Aurora Store (Nightly) +

An open-source Google Play Store client.

+
+
+
+ +
+
+
+
+
+
+
+ F-Droid +

An installable catalogue of FOSS Android applications.

+
+
+
+ +
+
+
+
+
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/blend-settings/src/pages/overlay.html b/blend-settings/src/pages/system.html similarity index 98% rename from blend-settings/src/pages/overlay.html rename to blend-settings/src/pages/system.html index 52aab29..4e4b9a3 100644 --- a/blend-settings/src/pages/overlay.html +++ b/blend-settings/src/pages/system.html @@ -61,4 +61,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/blend-system b/blend-system index 310e6f6..738866a 100755 --- a/blend-system +++ b/blend-system @@ -87,19 +87,6 @@ def current_state(): _state = int(s[5:-7]) return _state -def load_overlay(): - if os.path.isfile('/blend/states/.load_prev_state') and os.path.isfile(f'/blend/states/state{current_state()}.tar.gz'): - load_prev_state() - os.remove('/blend/states/.load_prev_state') - subprocess.call(['mkdir', '-p', '/blend/overlay/current/usr'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - subprocess.call(['rm', '-rf', '/blend/overlay/workdir'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - subprocess.call(['mkdir', '-p', '/blend/overlay/workdir'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - subprocess.call(['touch', '/blend/overlay/current/usr/.blend_overlay'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - subprocess.call(['chattr', '+i', '/blend/overlay/current/usr/.blend_overlay'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - subprocess.call(['mount', '-t', 'overlay', 'overlay', '-o', 'rw,lowerdir=/usr,upperdir=/blend/overlay/current/usr,workdir=/blend/overlay/workdir', - '/usr', '-o', 'index=off'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - info('mounted overlay') - def save_state(): subprocess.call(['mkdir', '-p', '/blend/states'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) state = current_state() + 1 diff --git a/blend.hook b/blend.hook index 7887320..d38921b 100644 --- a/blend.hook +++ b/blend.hook @@ -15,8 +15,16 @@ run_latehook() { rm -f "/new_root/blend/states/state${c}.tar.gz" "/new_root/blend/states/.load_prev_state" fi - mkdir -p /new_root/blend/overlay/current/usr /new_root/usr - rm -rf /new_root/blend/overlay/workdir - mkdir -p /new_root/blend/overlay/workdir - mount -t overlay overlay -o 'lowerdir=/new_root/usr,upperdir=/new_root/blend/overlay/current/usr,workdir=/new_root/blend/overlay/workdir' /new_root/usr -o index=off + mkdir -p /new_root/blend/overlay/current/usr/bin \ + /new_root/blend/overlay/current/usr/sbin \ + /new_root/blend/overlay/current/usr/share/plymouth + + mkdir -p /new_root/usr/bin \ + /new_root/usr/sbin \ + /new_root/usr/share/plymouth + rm -rf /new_root/blend/overlay/workdir_1 /new_root/blend/overlay/workdir_2 /new_root/blend/overlay/workdir_3 + mkdir -p /new_root/blend/overlay/workdir_1 /new_root/blend/overlay/workdir_2 /new_root/blend/overlay/workdir_3 + mount -t overlay overlay -o 'lowerdir=/new_root/usr/bin,upperdir=/new_root/blend/overlay/current/usr/bin,workdir=/new_root/blend/overlay/workdir_1' /new_root/usr/bin -o index=off + mount -t overlay overlay -o 'lowerdir=/new_root/usr/sbin,upperdir=/new_root/blend/overlay/current/usr/sbin,workdir=/new_root/blend/overlay/workdir_2' /new_root/usr/sbin -o index=off + mount -t overlay overlay -o 'lowerdir=/new_root/usr/share/plymouth,upperdir=/new_root/blend/overlay/current/usr/share/plymouth,workdir=/new_root/blend/overlay/workdir_3' /new_root/usr/share/plymouth -o index=off } From de7e60e65ed8162c170596839b6dfb84d4259bec Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Mon, 17 Apr 2023 12:56:58 +0530 Subject: [PATCH 035/121] Improve immutability and snapshots, blend-settings; Revamp blend --- README.md | 4 +- blend | 67 +-------- blend-files | 100 ++++++++----- blend-settings/src/index.html | 8 +- blend-settings/src/internal/js/android.js | 175 +++++++++++----------- blend-settings/src/internal/js/system.js | 4 + blend-settings/src/pages/android.html | 64 +++----- blend-settings/src/pages/containers.html | 5 +- blend-settings/src/pages/system.html | 10 +- blend-settings/src/pages/terminal.html | 4 + blend-system | 4 +- blend.hook | 6 +- init-blend | 84 ++++++++++- 13 files changed, 288 insertions(+), 247 deletions(-) diff --git a/README.md b/README.md index efe7c86..6e9ea10 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@ This repository also contains **blend-settings**, a tool for configuring blend a ## Credits -The `init-blend` file in this repository uses a few lines (two sections) uses from distrobox's init script. These lines have been marked and attributed appropriately, and are licensed under [the GPL-3.0 license](https://github.com/89luca89/distrobox/blob/main/COPYING.md). +The `init-blend` file in this repository uses a few lines (the sections have been clearly) uses from distrobox's init script. These lines have been marked and attributed appropriately, and are licensed under [the GPL-3.0 license](https://github.com/89luca89/distrobox/blob/main/COPYING.md). + +I would also like to thank Luca Di Maio from Distrobox for NVIDIA driver support in containers. Aside from these lines, all the other code in this repository has been written by me (rs2009). `blend-settings` is based on [Modren](https://github.com/RudraSwat/modren), a software store I (rs2009) had written long ago, and is licensed under the same license as the rest of the code in this repository, [the GPL-3.0 license](https://github.com/blend-os/blend/blob/main/LICENSE). diff --git a/blend b/blend index aa5d82b..fff273e 100755 --- a/blend +++ b/blend @@ -111,7 +111,7 @@ def check_container(name): return True return False -def check_container_status(name): +def check_container_status(name): return host_get_output("podman inspect --type container " + name + " --format \"{{.State.Status}}\"") def core_start_container(name): @@ -128,7 +128,7 @@ def core_start_container(name): logproc = pexpect.spawn('podman', args=['logs', '-f', '--since', str(start_time), name], timeout=300) logproc.logfile_read = sys.stdout.buffer - logproc.expect('Completed container setup') + logproc.expect('Completed container setup.') logproc.terminate() def core_create_container(): @@ -189,11 +189,6 @@ def core_create_container(): core_start_container(name) - if distro == 'arch': - core_run_container('sudo pacman -Sy') - core_run_container('sudo pacman --noconfirm -Syu --needed git base-devel') - core_run_container('TEMP_DIR="$(mktemp -d)"; cd "${TEMP_DIR}"; git clone https://aur.archlinux.org/yay.git; cd yay; makepkg --noconfirm -si; rm -rf "${TEMP_DIR}"') - core_get_output = lambda cmd: subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode('UTF-8').strip() host_get_output = lambda cmd: subprocess.run(['bash', '-c', cmd], @@ -399,57 +394,11 @@ if os.geteuid() == 0 and os.environ['BLEND_ALLOW_ROOT'] == None: exit(1) description = f''' -{colors.bold}{colors.fg.purple}Usage:{colors.reset} - blend [command] [options] [arguments] - {colors.bold}{colors.fg.purple}Version:{colors.reset} {__version}{colors.bold} -{colors.bold}{colors.bg.purple}blend{colors.reset}{colors.bold} is a package manager for {colors.bg.purple}blendOS{colors.reset}{colors.bold}, which includes support for Arch, Ubuntu and Fedora packages.{colors.reset} +Use the 'blendOS Settings' app to create and manage Linux containers, Android apps and immutability configuration. -{colors.bold}{colors.fg.purple}default distro{colors.reset}: {colors.bold}{colors.fg.lightblue}arch{colors.reset} (default container's name is the same as that of the default distro) - -Here's a list of the supported distros: -{colors.bold}1.{colors.reset} arch -{colors.bold}2.{colors.reset} fedora-rawhide -{colors.bold}3.{colors.reset} ubuntu-22.04 -{colors.bold}4.{colors.reset} ubuntu-22.10 -(debian support is coming soon) - -You can use any of these distros by passing the option {colors.bold}--distro=[NAME OF THE DISTRO]{colors.reset}. - -You can even install a supported desktop environment in a blend container (run `blend install-de [DESKTOP ENVIRONMENT NAME]` to install your favorite desktop environment). - -However, this feature is still somewhat experimental, and some apps might be buggy. - -Here's a list of the supported desktop environments: -{colors.bold}1.{colors.reset} gnome -{colors.bold}2.{colors.reset} mate -(support for many more DEs is coming soon) - -{colors.bold}{colors.fg.lightblue}arch{colors.reset} also supports AUR packages, for an extremely large app catalog. - -{colors.bold}{colors.fg.purple}available commands{colors.reset}: - {colors.bold}help{colors.reset} Show this help message and exit. - {colors.bold}version{colors.reset} Show version information and exit. - {colors.bold}enter{colors.reset} Enter the container shell. - {colors.bold}install{colors.reset} Install packages inside a container. - {colors.bold}remove{colors.reset} Remove packages inside a managed container. - {colors.bold}create-container{colors.reset} Create a container managed by blend. - {colors.bold}remove-container{colors.reset} Remove a container managed by blend. - {colors.bold}list-containers{colors.reset} List all the containers managed by blend. - {colors.bold}start-containers{colors.reset} Start all the container managed by blend. - {colors.bold}sync{colors.reset} Sync list of available packages from repository. - {colors.bold}search{colors.reset} Search for packages in a managed container. - {colors.bold}show{colors.reset} Show details about a package. - {colors.bold}update{colors.reset} Update all the packages in a managed container. - -{colors.bold}{colors.fg.purple}options for commands{colors.reset}: - {colors.bold}-cn CONTAINER NAME, --container-name CONTAINER NAME{colors.reset} - set the container name (the default is the name of the distro) - {colors.bold}-d DISTRO, --distro DISTRO{colors.reset} - set the distro name (supported: arch fedora-rawhide ubuntu-22.04 ubuntu-22.10; default is arch) - {colors.bold}-y, --noconfirm{colors.reset} assume yes for all questions - {colors.bold}-v, --version{colors.reset} show version information and exit +You can install and submit web apps from the Web Store. ''' epilog = f''' @@ -458,17 +407,13 @@ epilog = f''' parser = argparse.ArgumentParser(description=description, usage=argparse.SUPPRESS, epilog=epilog, formatter_class=argparse.RawTextHelpFormatter) -command_map = { 'install': install_blend, - 'remove': remove_blend, - 'enter': enter_container, +command_map = { 'enter': enter_container, + 'exec': enter_container, 'create-container': core_create_container, 'remove-container': remove_container, 'list-containers': list_containers, 'start-containers': start_containers, 'sync': sync_blends, - 'update': update_blends, - 'search': search_blend, - 'show': show_blend, 'help': 'help', 'version': 'version' } parser.add_argument('command', choices=command_map.keys(), help=argparse.SUPPRESS) diff --git a/blend-files b/blend-files index f9bca35..08e7bf5 100755 --- a/blend-files +++ b/blend-files @@ -1,9 +1,17 @@ #!/usr/bin/env python3 -import os, sys, yaml, time, getpass, shutil, fileinput, subprocess +import os +import sys +import yaml +import time +import getpass +import shutil +import fileinput +import subprocess + def get_containers(): - container_list = subprocess.run(['sudo', '-u', user, 'podman', 'ps', '-a', '--no-trunc', '--size', '--sort=created', '--format', + container_list = subprocess.run(['sudo', '-u', user, 'podman', 'ps', '-a', '--no-trunc', '--sort=created', '--format', '{{.Names}}'], stdout=subprocess.PIPE).stdout.decode('utf-8').strip().split('\n') try: @@ -22,6 +30,7 @@ def get_containers(): except: return container_list + def list_use_container_bin(): try: with open(os.path.expanduser('~/.config/blend/config.yaml')) as config_file: @@ -30,23 +39,25 @@ def list_use_container_bin(): except: return [] + def check_if_present(attr, desktop_str): for l in desktop_str: if l.startswith(attr + '='): return True return False + def which(bin): results = [] for dir in os.environ.get('PATH').split(':'): if os.path.isdir(dir): - for i in os.listdir(dir): - if os.path.basename(bin) == i: - results.append(os.path.join(dir, i)) + if os.path.basename(bin) in os.listdir(dir): + results.append(os.path.join(dir, os.path.basename(bin))) if results == []: return None return results + def create_container_binaries(): _binaries = [] remove_binaries = [] @@ -54,17 +65,18 @@ def create_container_binaries(): for c in _list: c = c.strip() for i in con_get_output(c, '''find /usr/bin -type f -printf "%P\n" 2>/dev/null; - find /usr/local/bin -type f -printf "%P\n" 2>/dev/null; - find /usr/sbin -type f -printf "%P\n" 2>/dev/null;''').split('\n'): + find /usr/local/bin -type f -printf "%P\n" 2>/dev/null;''').split('\n'): i = i.strip() - os.makedirs(os.path.expanduser(f'~/.local/bin/blend_{c}'), exist_ok=True) + os.makedirs(os.path.expanduser( + f'~/.local/bin/blend_{c}'), exist_ok=True) i_present = False orig_which_out = which(os.path.basename(i)) which_out = None if orig_which_out != None: which_out = orig_which_out.copy() try: - which_out.remove(os.path.expanduser(f'~/.local/bin/blend_bin/{os.path.basename(i)}')) + which_out.remove(os.path.expanduser( + f'~/.local/bin/blend_bin/{os.path.basename(i)}')) except ValueError: pass if which_out == []: @@ -74,13 +86,17 @@ def create_container_binaries(): if os.path.basename(i) != 'host-spawn' and i != '' and not i_present: with open(os.path.expanduser(f'~/.local/bin/blend_{c}/{os.path.basename(i)}.tmp'), 'w') as f: - f.write('#!/bin/sh\n') - f.write(f'# blend container: {i}\n') + f.write('#!/bin/bash\n') + f.write(f'# blend container: {c};{i}\n') if os.path.basename(i) in _exceptions: f.write(f'# EXCEPTION\n') - f.write(f'BLEND_ALLOW_ROOT= BLEND_NO_CHECK= blend enter -cn {c} -- {i} "$@"\n') + f.write('[ -f /run/.containerenv ] && { if [[ -e "/usr/bin/' + os.path.basename(i) + '" ]] || [[ -e "/usr/local/bin/' + os.path.basename(i) + '" ]]; then if [[ -e "/usr/bin/' + os.path.basename(i) + '" ]]; then /usr/bin/' + os.path.basename( + i) + ' "$@"; elif [[ -e "/usr/local/bin/' + os.path.basename(i) + '" ]]; then /usr/local/bin/' + os.path.basename(i) + ' "$@"; fi; exit $?; else echo "This command can be accessed from the host, or from the container \'' + c + '\'."; exit 127; fi } || :\n') + f.write( + f'BLEND_ALLOW_ROOT= BLEND_NO_CHECK= blend enter -cn {c} -- {os.path.basename(i)} "$@"\n') # XXX: make this bit fully atomic - os.chmod(os.path.expanduser(f'~/.local/bin/blend_{c}/{os.path.basename(i)}.tmp'), 0o775) + os.chmod(os.path.expanduser( + f'~/.local/bin/blend_{c}/{os.path.basename(i)}.tmp'), 0o775) subprocess.call(['mv', os.path.expanduser(f'~/.local/bin/blend_{c}/{os.path.basename(i)}.tmp'), os.path.expanduser(f'~/.local/bin/blend_{c}/{os.path.basename(i)}')]) _binaries.append((c, os.path.basename(os.path.basename(i)))) @@ -89,11 +105,15 @@ def create_container_binaries(): for c, i in _binaries: try: - os.symlink(os.path.expanduser(f'~/.local/bin/blend_{c}/{i}'), os.path.expanduser(f'~/.local/bin/blend_bin/{i}')) + if i == 'apt': + print(c, i) + os.symlink(os.path.expanduser( + f'~/.local/bin/blend_{c}/{i}'), os.path.expanduser(f'~/.local/bin/blend_bin/{i}')) except FileExistsError: - if not subprocess.call(['grep', '-q', f'^# container: {c}$', os.path.expanduser(f'~/.local/bin/blend_bin/{i}')]): + if subprocess.call(['grep', '-q', f'^# container: {c};{i}$', os.path.expanduser(f'~/.local/bin/blend_bin/{i}')], shell=False): os.remove(os.path.expanduser(f'~/.local/bin/blend_bin/{i}')) - os.symlink(os.path.expanduser(f'~/.local/bin/blend_{c}/{i}'), os.path.expanduser(f'~/.local/bin/blend_bin/{i}')) + os.symlink(os.path.expanduser( + f'~/.local/bin/blend_{c}/{i}'), os.path.expanduser(f'~/.local/bin/blend_bin/{i}')) for i in remove_binaries: try: @@ -103,12 +123,15 @@ def create_container_binaries(): for b in os.listdir(os.path.expanduser(f'~/.local/bin/blend_bin')): if [_b for _b in _binaries if _b[1] == b] == []: - os.remove(os.path.join(os.path.expanduser(f'~/.local/bin/blend_bin'), b)) + os.remove(os.path.join(os.path.expanduser( + f'~/.local/bin/blend_bin'), b)) + def create_container_applications(): _apps = [] - os.makedirs(os.path.expanduser(f'~/.local/share/applications'), exist_ok=True) + os.makedirs(os.path.expanduser( + f'~/.local/share/applications'), exist_ok=True) for c in _list: c = c.strip() @@ -116,11 +139,13 @@ def create_container_applications(): orig_path = i.strip() i = os.path.basename(orig_path) i_present = (os.path.isfile(f'/usr/share/applications/{i}') or os.path.isfile(f'/usr/local/share/applications/{i}') - or os.path.isfile(os.path.expanduser(f'~/.local/share/applications/{i}'))) + or os.path.isfile(os.path.expanduser(f'~/.local/share/applications/{i}'))) if i != '' and not i_present: with open(os.path.expanduser(f'~/.local/share/applications/blend;{i}'), 'w') as f: - _ = con_get_output(c, f"sudo sed -i '/^DBusActivatable=/d' {orig_path}") - _ = con_get_output(c, f"sudo sed -i '/^TryExec=/d' {orig_path}") + _ = con_get_output( + c, f"sudo sed -i '/^DBusActivatable=/d' {orig_path}") + _ = con_get_output( + c, f"sudo sed -i '/^TryExec=/d' {orig_path}") contents = con_get_output(c, f'cat {orig_path}') f.write(contents) for line in fileinput.input(os.path.expanduser(f'~/.local/share/applications/blend;{i}'), inplace=True): @@ -129,16 +154,19 @@ def create_container_applications(): elif line.strip().startswith('Icon='): if '/' in line: line = line.strip() - _ = con_get_output(c, f"mkdir -p ~/.local/share/blend/icons/file/\"{c}_{i}\"; cp {line[5:]} ~/.local/share/blend/icons/file/\"{c}_{i}\"") + _ = con_get_output( + c, f"mkdir -p ~/.local/share/blend/icons/file/\"{c}_{i}\"; cp {line[5:]} ~/.local/share/blend/icons/file/\"{c}_{i}\"") line = f'Icon={os.path.expanduser("~/.local/share/blend/icons/file/" + c + "_" + i + "/" + os.path.basename(line[5:]))}\n' else: line = line.strip() icons = con_get_output(c, f'''find /usr/share/icons /usr/share/pixmaps /var/lib/flatpak/exports/share/icons \\ -type f -iname "*{line[5:]}*" 2> /dev/null | sort''').split('\r\n') - _ = con_get_output(c, f"mkdir -p ~/.local/share/blend/icons/\"{c}_{i}\"; cp {icons[0]} ~/.local/share/blend/icons/\"{c}_{i}\"") + _ = con_get_output( + c, f"mkdir -p ~/.local/share/blend/icons/\"{c}_{i}\"; cp {icons[0]} ~/.local/share/blend/icons/\"{c}_{i}\"") line = f'Icon={os.path.expanduser("~/.local/share/blend/icons/" + c + "_" + i + "/" + os.path.basename(icons[0]))}\n' sys.stdout.write(line) - os.chmod(os.path.expanduser(f'~/.local/share/applications/blend;{i}'), 0o775) + os.chmod(os.path.expanduser( + f'~/.local/share/applications/blend;{i}'), 0o775) _apps.append((c, i)) del _ @@ -146,7 +174,9 @@ def create_container_applications(): if a.startswith('blend;'): a = a.removeprefix('blend;') if [_a for _a in _apps if _a[1] == a] == []: - os.remove(os.path.expanduser(f'~/.local/share/applications/blend;{a}')) + os.remove(os.path.expanduser( + f'~/.local/share/applications/blend;{a}')) + def create_container_sessions(type='xsessions'): session_dir = f'/usr/share/{type}' @@ -176,10 +206,13 @@ def create_container_sessions(type='xsessions'): continue sys.stdout.write(line) - os.chmod(os.path.expanduser(f'{session_dir}/blend-{c};{i}'), 0o775) + os.chmod(os.path.expanduser( + f'{session_dir}/blend-{c};{i}'), 0o775) + + +def con_get_output(name, cmd): return subprocess.run(['sudo', '-u', user, 'podman', 'exec', '--user', getpass.getuser(), '-it', name, 'bash', '-c', cmd], + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode('UTF-8').strip() -con_get_output = lambda name, cmd: subprocess.run(['sudo', '-u', user, 'podman', 'exec', '--user', getpass.getuser(), '-it', name, 'bash', '-c', cmd], - stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode('UTF-8').strip() user = getpass.getuser() @@ -188,15 +221,6 @@ try: except: pass -try: - if sys.argv[1] == 'sessions': - _list = get_containers() - create_container_sessions(type='xsessions') - create_container_sessions(type='wayland-sessions') - exit(0) -except IndexError: - pass - for c in get_containers(): c = c.strip() subprocess.call(['podman', 'start', c]) @@ -207,4 +231,4 @@ while True: create_container_binaries() create_container_applications() - time.sleep(6) + time.sleep(15) diff --git a/blend-settings/src/index.html b/blend-settings/src/index.html index 30506df..bef71a3 100644 --- a/blend-settings/src/index.html +++ b/blend-settings/src/index.html @@ -13,10 +13,10 @@
+
+
+
+ Disable app grouping +

+ Do not automatically group apps of different categories/web apps/Android apps. +

+
+
+
+ +
+
+
+
From fddf7f86ae59852262590e7d859c4c971e3d2495 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sat, 22 Apr 2023 19:19:40 +0530 Subject: [PATCH 049/121] Increase gap between commands for WayDroid --- blend-settings/src/internal/js/android.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/blend-settings/src/internal/js/android.js b/blend-settings/src/internal/js/android.js index e5590ca..ecab68a 100644 --- a/blend-settings/src/internal/js/android.js +++ b/blend-settings/src/internal/js/android.js @@ -8,13 +8,14 @@ function init_waydroid() { require('child_process').spawn('sh', ['-c', 'waydroid session start & disown']) setTimeout(() => { require('child_process').spawnSync('waydroid', ['prop', 'set', 'persist.waydroid.multi_windows', 'true']) + require('child_process').spawnSync('pkexec', ['waydroid', 'shell', 'pm', 'disable', 'com.android.inputmethod.latin']) if (require('child_process').spawnSync('sh', ['-c', 'LC_ALL=C glxinfo | grep "^OpenGL renderer string: "']).stdout.includes('NVIDIA')) { require('child_process').spawnSync('sh', ['-c', 'echo "ro.hardware.gralloc=default" | pkexec tee -a /var/lib/waydroid/waydroid.cfg']) require('child_process').spawnSync('sh', ['-c', 'echo "ro.hardware.egl=swiftshader" | pkexec tee -a /var/lib/waydroid/waydroid.cfg']) } require('child_process').spawn('sh', ['-c', 'pkexec waydroid upgrade -o; waydroid session stop; waydroid session start']) - setTimeout(() => { require('child_process').spawnSync('pkexec', ['waydroid', 'shell', 'pm', 'disable', 'com.android.inputmethod.latin']); postMessage('success') }, 5000) - }, 2000) + setTimeout(() => { postMessage('success') }, 1000) + }, 8000) ` ) init_worker.onmessage = e => { @@ -24,6 +25,24 @@ function init_waydroid() { document.getElementById('first-time-waydroid').classList.remove('d-none') } } + if (require('child_process').spawnSync('waydroid', ['app', 'list']).stdout.includes('com.aurora.store')) { + document.getElementById('aurora-store-btn').outerHTML = + `` + } else { + document.getElementById('aurora-store-btn').outerHTML = + `` + } + if (require('child_process').spawnSync('waydroid', ['app', 'list']).stdout.includes('org.fdroid.fdroid')) { + document.getElementById('f-droid-btn').outerHTML = + `` + } else { + document.getElementById('f-droid-btn').outerHTML = + `` + } } function install_aurora_store() { From 5e65e6c06eaf7b67253b96427144120d957e4147 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Tue, 25 Apr 2023 16:44:01 +0530 Subject: [PATCH 050/121] Rework immutability and overlays --- .gitignore | 4 +- blend-settings/src/index.html | 2 +- blend-system | 53 ++-- blend-system.service | 10 - blend.hook | 72 +++-- blend.install | 3 +- overlayfs-tools/README.md | 42 +++ overlayfs-tools/logic.c | 581 ++++++++++++++++++++++++++++++++++ overlayfs-tools/logic.h | 37 +++ overlayfs-tools/main.c | 266 ++++++++++++++++ overlayfs-tools/makefile | 23 ++ overlayfs-tools/sh.c | 98 ++++++ overlayfs-tools/sh.h | 20 ++ 13 files changed, 1141 insertions(+), 70 deletions(-) delete mode 100644 blend-system.service mode change 100644 => 100755 blend.hook create mode 100755 overlayfs-tools/README.md create mode 100755 overlayfs-tools/logic.c create mode 100755 overlayfs-tools/logic.h create mode 100755 overlayfs-tools/main.c create mode 100755 overlayfs-tools/makefile create mode 100755 overlayfs-tools/sh.c create mode 100755 overlayfs-tools/sh.h diff --git a/.gitignore b/.gitignore index a48434a..2e10d03 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /blend-settings/node_modules/ /blend-settings/build/ /blend-settings/dist/ -/blend-settings/package-lock.json \ No newline at end of file +/blend-settings/package-lock.json +*.o +/overlayfs-tools/overlayfs-tools diff --git a/blend-settings/src/index.html b/blend-settings/src/index.html index bef71a3..314743d 100644 --- a/blend-settings/src/index.html +++ b/blend-settings/src/index.html @@ -18,7 +18,7 @@ Containers - + diff --git a/blend-system b/blend-system index 885e382..2591373 100755 --- a/blend-system +++ b/blend-system @@ -70,51 +70,39 @@ def info(msg): def error(err): print (colors.bold + colors.fg.red + '>> e: ' + colors.reset + colors.bold + err + colors.reset) -def load_prev_state(): - subprocess.call(['rm', '-rf', '/blend/overlay/current'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - subprocess.call(['mkdir', '-p', '/blend/overlay/current/usr'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - subprocess.call(['tar', '-xvpzf', f'/blend/states/state{current_state()}.tar.gz', '-C', '/blend/overlay/current/usr', '--numeric-owner'], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - subprocess.call(['rm', '-f', f'/blend/states/state{current_state()}.tar.gz'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - ### END def current_state(): _state = -1 - for s in os.listdir('/blend/states'): - if re.match(r'^state([0-9]+)\.tar\.gz$', s): + for s in os.listdir('/.states'): + if re.match(r'^state([0-9]+)\.squashfs$', s): if int(s[5:-7]) > _state: _state = int(s[5:-7]) return _state def save_state(): - subprocess.call(['mkdir', '-p', '/blend/states'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.call(['mkdir', '-p', '/.states'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) state = current_state() + 1 - subprocess.call(r"find /blend/states/ -type f -not -name 'state" + str(state - 1) + ".tar.gz' -print0 | xargs -0 -I {} rm {}", stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=True) - subprocess.call(['tar', '-C', '/blend/overlay/current/usr', '-cpzf', f'/blend/states/state{state}.tar.gz', '.'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.call(['bash', '-c', 'rm -f /.states/*.tmp']) + + if subprocess.call(['mksquashfs', '/usr', f'/.states/state{state}.squashfs.tmp', '-no-compression'], stdout=sys.stdout, stderr=sys.stderr) == 0: + subprocess.call(['rm', '-rf', 'add-squashfs'], cwd='/tmp') + subprocess.call(['mkdir', '-p', 'add-squashfs'], cwd='/tmp') + subprocess.call(['cp', '-a', '/var/lib', 'add-squashfs/varlib'], cwd='/tmp') + if subprocess.call(['mksquashfs', 'add-squashfs', f'/.states/state{state}.squashfs.tmp', '-no-compression'], cwd='/tmp') == 0: + subprocess.call(['mv', f'/.states/state{state}.squashfs.tmp', f'/.states/state{state}.squashfs']) + else: + error('state creation failed') + exit(1) + else: + error('state creation failed') + exit(1) + info(f'saved state {state}') -def autosave_state(): - while True: - if not os.path.isfile('/blend/states/.disable_states'): - save_state() - time.sleep(12*60*60) # XXX: make this configurable - -def toggle_states(): - if os.path.isfile('/blend/states/.disable_states'): - os.remove('/blend/states/.disable_states') - info('enabled saving states automatically (every 6 hours; this will be configurable in future releases)') - else: - subprocess.call(['touch', '/blend/states/.disable_states'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - info('disabled saving states automatically') - def rollback(): - if current_state() == -1: - error('no states present') - exit(1) - subprocess.call(['touch', '/blend/states/.load_prev_state'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - info(f'will rollback to the previous state on the next boot') + info("Rollback hasn't been implemented yet.") description = f''' {colors.bold}{colors.fg.purple}Usage:{colors.reset} @@ -126,7 +114,6 @@ description = f''' {colors.bold}help{colors.reset} Show this help message and exit. {colors.bold}version{colors.reset} Show version information and exit. {colors.bold}save-state{colors.reset} Save the current state (backup). - {colors.bold}toggle-states{colors.reset} Enable/disable automatic state creation (you can still manually save states). {colors.bold}rollback{colors.reset} Rollback to previous state. {colors.bold}{colors.fg.purple}options for commands{colors.reset}: @@ -142,8 +129,6 @@ parser = argparse.ArgumentParser(description=description, usage=argparse.SUPPRES command_map = { 'help': 'help', 'version': 'version', 'save-state': save_state, - 'toggle-states': toggle_states, - 'autosave-state': autosave_state, 'rollback': rollback } parser.add_argument('command', choices=command_map.keys(), help=argparse.SUPPRESS) parser.add_argument('-v', '--version', action='version', version=f'%(prog)s {__version}', help=argparse.SUPPRESS) diff --git a/blend-system.service b/blend-system.service deleted file mode 100644 index 51f3b06..0000000 --- a/blend-system.service +++ /dev/null @@ -1,10 +0,0 @@ -[Unit] -Description=Save system state - -[Service] -Type=simple -ExecStart=/usr/bin/blend-system autosave-state -User=root - -[Install] -WantedBy=multi-user.target diff --git a/blend.hook b/blend.hook old mode 100644 new mode 100755 index 3be4e3a..0eb8f44 --- a/blend.hook +++ b/blend.hook @@ -1,30 +1,56 @@ #!/bin/bash run_latehook() { - if [[ -f /new_root/blend/states/.load_prev_state ]] && compgen -G "/new_root/blend/states/state+([0-9]).tar.gz" >/dev/null; then - rm -rf /new_root/blend/overlay/current - mkdir -p /new_root/blend/overlay/current/usr - c=0 - for i in $(compgen -G "/new_root/blend/states/state*.tar.gz"); do - n="${i:19:-7}" - if [[ "$n" -gt "$c" ]]; then - c="$n" - fi - done - tar -xvpzf "/new_root/blend/states/state${c}.tar.gz" -C "/new_root/blend/overlay/current/usr" --numeric-owner &>/dev/null - rm -f "/new_root/blend/states/state${c}.tar.gz" "/new_root/blend/states/.load_prev_state" + echo + + if [[ "$abort_staging" == true ]]; then + echo '[ BLEND ] Not applying system changes made in previous boot.' + rm -rf '/new_root/.upperdir' '/new_root/.workdir'; mkdir -p '/new_root/.upperdir' '/new_root/.workdir' fi - mkdir -p /new_root/blend/overlay/current/usr/bin \ - /new_root/blend/overlay/current/usr/sbin \ - /new_root/blend/overlay/current/usr/share + if [[ -d "/new_root/blend/overlay/current" ]]; then + echo '[ BLEND ] Detected old version of overlay implementation, switching.' + rm -rf /new_root/.upperdir /new_root/.workdir + mv /new_root/blend/overlay/current/usr /new_root/.upperdir + rm -rf /new_root/blend + fi - mkdir -p /new_root/usr/bin \ - /new_root/usr/sbin \ - /new_root/usr/share - rm -rf /new_root/blend/overlay/workdir_1 /new_root/blend/overlay/workdir_2 /new_root/blend/overlay/workdir_3 - mkdir -p /new_root/blend/overlay/workdir_1 /new_root/blend/overlay/workdir_2 /new_root/blend/overlay/workdir_3 - mount -t overlay overlay -o 'lowerdir=/new_root/usr/bin,upperdir=/new_root/blend/overlay/current/usr/bin,workdir=/new_root/blend/overlay/workdir_1' /new_root/usr/bin -o index=off - mount -t overlay overlay -o 'lowerdir=/new_root/usr/sbin,upperdir=/new_root/blend/overlay/current/usr/sbin,workdir=/new_root/blend/overlay/workdir_2' /new_root/usr/sbin -o index=off - mount -t overlay overlay -o 'lowerdir=/new_root/usr/share,upperdir=/new_root/blend/overlay/current/usr/share,workdir=/new_root/blend/overlay/workdir_3' /new_root/usr/share -o index=off + # Broken attempt at getting rollback and snapshots working. + # + # if [[ -L "/new_root/.states/rollback.squashfs" ]] && [[ -e "/new_root/.states/rollback.squashfs" ]]; then + # echo -n '[ BLEND ] Rolling back to selected state. Do __not__ power off or reboot.' + # echo + # cd /new_root + # unsquashfs /new_root/.states/rollback.squashfs && ( + # for i in bin include lib lib32 share src; do + # rm -rf rm -rf /new_root/.workdir/"$i" rm -rf /new_root/.upperdir/"$i" /new_root/usr/"$i" + # mv squashfs-root/"$i" /new_root/usr + # done + # rm -rf /new_root/.workdir/varlib /new_root/.upperdir/varlib /new_root/var/lib + # mkdir -p /new_root/var/lib + # mv squashfs-root/varlib /new_root/var/varlib + # echo ' - SUCCESS ' + # echo + # ); cd .. + # fi + + for i in bin include lib lib32 share src; do + echo -n "[ BLEND ] Setting up /usr/${i} overlay (applying changes)." + rm -rf /new_root/.workdir/"$i" + mkdir -p /new_root/.upperdir/"$i" /new_root/.workdir/"$i" /new_root/usr/"$i" /new_root/tmp + cd /new_root/tmp; overlayfs-tools merge -l /new_root/usr/$i -u /new_root/.upperdir/$i &>/dev/null; chmod 755 ./overlay-tools-*; ./overlay-tools-* &>/dev/null; rm -f ./overlay-tools-*; cd / + mkdir -p /new_root/.upperdir/"$i" + mount -t overlay overlay -o 'lowerdir=/new_root/usr/'$i',upperdir=/new_root/.upperdir/'$i',workdir=/new_root/.workdir/'$i /new_root/usr/"$i" -o index=off + echo " - SUCCESS" + done + + echo + echo -n "[ BLEND ] Setting up /var/lib overlay (applying changes)." + rm -rf /new_root/.workdir/varlib + mkdir -p /new_root/.upperdir/varlib /new_root/.workdir/varlib /new_root/var/lib /new_root/tmp + cd /new_root/tmp; overlayfs-tools merge -l /new_root/var/lib -u /new_root/.upperdir/varlib &>/dev/null; chmod 755 ./overlay-tools-*; ./overlay-tools-* &>/dev/null; rm -f ./overlay-tools-*; cd / + mkdir -p /new_root/.upperdir/varlib + mount -t overlay overlay -o 'lowerdir=/new_root/var/lib,upperdir=/new_root/.upperdir/varlib,workdir=/new_root/.workdir/varlib' /new_root/var/lib -o index=off + echo ' - SUCCESS' + echo } diff --git a/blend.install b/blend.install index 2eb9e15..434a035 100644 --- a/blend.install +++ b/blend.install @@ -5,12 +5,13 @@ build() { add_module overlay add_binary bash add_binary tar + add_binary overlayfs-tools add_runscript } help() { cat < A directory is made opaque by setting the xattr "trusted.overlay.opaque" to "y". + +However, only users with `CAP_SYS_ADMIN` can read `trusted.*` extended attributes. + +Warnings / limitations +-------- + +- Only works for regular files and directories. Do not use it on OverlayFS with device files, socket files, etc.. +- Hard links may be broken (i.e. resulting in duplicated independent files). +- File owner, group and permission bits will be preserved. File timestamps, attributes and extended attributes might be lost. +- This program only works for OverlayFS with only one lower layer. +- It is recommended to have the OverlayFS unmounted before running this program. diff --git a/overlayfs-tools/logic.c b/overlayfs-tools/logic.c new file mode 100755 index 0000000..47ebfaa --- /dev/null +++ b/overlayfs-tools/logic.c @@ -0,0 +1,581 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "logic.h" +#include "sh.h" + +// exactly the same as in linux/fs.h +#define WHITEOUT_DEV 0 + +// exact the same as in fs/overlayfs/overlayfs.h +const char *ovl_opaque_xattr = "trusted.overlay.opaque"; +const char *ovl_redirect_xattr = "trusted.overlay.redirect"; +const char *ovl_metacopy_xattr = "trusted.overlay.metacopy"; + +#define MIN(X,Y) ((X) < (Y) ? (X) : (Y)) + +#define TRAILING_SLASH(ftype) (((ftype) == S_IFDIR) ? "/" : "") + +static inline mode_t file_type(const struct stat *status) { + return status->st_mode & S_IFMT; +} + +const char *ftype_name(mode_t type) { + switch (type) { + case S_IFDIR: + return "directory"; + case S_IFREG: + return "regular file"; + case S_IFLNK: + return "symbolic link"; + default: + return "special file"; + } +} + +const char *ftype_name_plural(mode_t type) { + switch (type) { + case S_IFDIR: + return "Directories"; + case S_IFREG: + return "Files"; + case S_IFLNK: + return "Symbolic links"; + default: + return "Special files"; + } +} + +static inline bool is_whiteout(const struct stat *status) { + return (file_type(status) == S_IFCHR) && (status->st_rdev == WHITEOUT_DEV); +} + +static inline mode_t permission_bits(const struct stat *status) { // not used yet. I haven't decided how to treat permission bit changes + return status->st_mode & (S_IRWXU | S_IRWXG | S_IRWXO | S_ISVTX); +} + +int is_opaque(const char *path, bool *output) { + char val; + ssize_t res = getxattr(path, ovl_opaque_xattr, &val, 1); + if ((res < 0) && (errno != ENODATA)) { + return -1; + } + *output = (res == 1 && val == 'y'); + return 0; +} + +int is_redirect(const char *path, bool *output) { + ssize_t res = getxattr(path, ovl_redirect_xattr, NULL, 0); + if ((res < 0) && (errno != ENODATA)) { + fprintf(stderr, "File %s redirect xattr can not be read.\n", path); + return -1; + } + *output = (res > 0); + return 0; +} + +int is_metacopy(const char *path, bool *output) { + ssize_t res = getxattr(path, ovl_metacopy_xattr, NULL, 0); + if ((res < 0) && (errno != ENODATA)) { + fprintf(stderr, "File %s metacopy xattr can not be read.\n", path); + return -1; + } + *output = (res >= 0); + return 0; +} + +// Treat redirect as opaque dir because it hides the tree in lower_path +// and we do not support following to redirected lower path +int is_opaquedir(const char *path, bool *output) { + bool opaque, redirect; + if (is_opaque(path, &opaque) < 0) { return -1; } + if (is_redirect(path, &redirect) < 0) { return -1; } + *output = opaque || redirect; + return 0; +} + +bool permission_identical(const struct stat *lower_status, const struct stat *upper_status) { + return (permission_bits(lower_status) == permission_bits(upper_status)) && (lower_status->st_uid == upper_status->st_uid) && (lower_status->st_gid == upper_status->st_gid); +} + +int read_chunk(int fd, char *buf, int len) { + ssize_t ret; + ssize_t remain = len; + while (remain > 0 && (ret = read(fd, buf, remain)) != 0) { + if (ret == -1) { + if (errno == EINTR) { + continue; + } + return -1; + } + remain -= ret; + buf += ret; + } + return len - remain; +} + +int regular_file_identical(const char *lower_path, const struct stat *lower_status, const char *upper_path, const struct stat *upper_status, bool *output) { + size_t blksize = (size_t) MIN(lower_status->st_blksize, upper_status->st_blksize); + if (lower_status->st_size != upper_status->st_size) { // different sizes + *output = false; + return 0; + } + bool metacopy, redirect; + if (is_metacopy(upper_path, &metacopy) < 0) { return -1; } + if (is_redirect(upper_path, &redirect) < 0) { return -1; } + if (metacopy) { + // metacopy means data is indentical, but redirect means it is not identical to lower_path + *output = !redirect; + return 0; + } + char lower_buffer[blksize]; + char upper_buffer[blksize]; + int lower_file = open(lower_path, O_RDONLY); + int upper_file = open(upper_path, O_RDONLY); + if (lower_file < 0) { + fprintf(stderr, "File %s can not be read for content.\n", lower_path); + return -1; + } + if (upper_file < 0) { + fprintf(stderr, "File %s can not be read for content.\n", upper_path); + return -1; + } + ssize_t read_lower; ssize_t read_upper; + do { // we can assume one will not reach EOF earlier than the other, as the file sizes are checked to be the same earlier + read_lower = read_chunk(lower_file, lower_buffer, blksize); + read_upper = read_chunk(upper_file, upper_buffer, blksize); + if (read_lower < 0) { + fprintf(stderr, "Error occured when reading file %s.\n", lower_path); + return -1; + } + if (read_upper < 0) { + fprintf(stderr, "Error occured when reading file %s.\n", upper_path); + return -1; + } + if (read_upper != read_lower) { // this should not happen as we've checked the sizes + fprintf(stderr, "Unexpected size difference: %s.\n", upper_path); + return -1; + } + if (memcmp(lower_buffer, upper_buffer, read_upper)) { *output = false; return 0; } // the output is by default false, but we still set it for ease of reading + } while (read_lower || read_upper); + *output = true; // now we can say they are identical + if (close(lower_file) || close(upper_file)) { return -1; } + return 0; +} + +int symbolic_link_identical(const char *lower_path, const char *upper_path, bool *output) { + char lower_buffer[PATH_MAX]; + char upper_buffer[PATH_MAX]; + ssize_t lower_len = readlink(lower_path, lower_buffer, PATH_MAX); + ssize_t upper_len = readlink(upper_path, upper_buffer, PATH_MAX); + if (lower_len < 0 || lower_len == PATH_MAX) { + fprintf(stderr, "Symbolic link %s cannot be resolved.\n", lower_path); + return -1; + } + if (upper_len < 0 || upper_len == PATH_MAX) { + fprintf(stderr, "Symbolic link %s cannot be resolved.\n", upper_path); + return -1; + } + lower_buffer[lower_len] = '\0'; + upper_buffer[upper_len] = '\0'; + *output = (strcmp(lower_buffer, upper_buffer) == 0); + return 0; +} + +static int vacuum_d(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { + bool opaque; + if (is_opaquedir(upper_path, &opaque) < 0) { return -1; } + if (opaque) { // TODO: sometimes removing opaque directory (and combine with lower directory) might be better + *fts_instr = FTS_SKIP; + } + return 0; +} + +static int vacuum_dp(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { + if (lower_status == NULL) { return 0; } // lower does not exist + if (file_type(lower_status) != S_IFDIR) { return 0; } + if (!permission_identical(lower_status, upper_status)) { return 0; } + bool opaque; + if (is_opaquedir(upper_path, &opaque) < 0) { + return -1; + } + if (opaque) { return 0; } + // this directory might be empty if all children are deleted in previous commands. but we simply don't test whether it's that case + return command(script_stream, "rmdir --ignore-fail-on-non-empty %U", upper_path); +} + +static int vacuum_f(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { + if (lower_status == NULL) { return 0; } // lower does not exist + if (file_type(lower_status) != S_IFREG) { return 0; } + if (!permission_identical(lower_status, upper_status)) { return 0; } + bool identical; + if (regular_file_identical(lower_path, lower_status, upper_path, upper_status, &identical) < 0) { + return -1; + } + if (!identical) { return 0; } + return command(script_stream, "rm %U", upper_path); +} + +static int vacuum_sl(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { + if (lower_status == NULL) { return 0; } // lower does not exist + if (file_type(lower_status) != S_IFLNK) { return 0; } + if (!permission_identical(lower_status, upper_status)) { return 0; } + bool identical; + if (symbolic_link_identical(lower_path, upper_path, &identical) < 0) { + return -1; + } + if (!identical) { return 0; } + return command(script_stream, "rm %U", upper_path); +} + +void print_only_in(const char *path) { + char *dirc = strdup(path); + char *basec = strdup(path); + char *dname = dirname(dirc); + char *bname = basename(basec); + printf("Only in %s: %s\n", dname, bname); + free(dirc); + free(basec); +} + +void print_removed(const char *lower_path, const size_t lower_root_len, mode_t lower_type) { + if (brief) { + print_only_in(lower_path); + } else { + printf("Removed: %s%s\n", &lower_path[lower_root_len], TRAILING_SLASH(lower_type)); + } +} + +void print_added(const char *lower_path, const size_t lower_root_len, const char *upper_path, mode_t upper_type) { + if (brief) { + print_only_in(upper_path); + } else { + printf("Added: %s%s\n", &lower_path[lower_root_len], TRAILING_SLASH(upper_type)); + } +} + +void print_replaced(const char *lower_path, const size_t lower_root_len, mode_t lower_type, const char *upper_path, mode_t upper_type) { + if (brief) { + printf("File %s is a %s while file %s is a %s\n", lower_path, ftype_name(lower_type), upper_path, ftype_name(upper_type)); + } else { + if (lower_type != S_IFDIR) { // dir removed already printed by list_deleted_files() + print_removed(lower_path, lower_root_len, lower_type); + } + print_added(lower_path, lower_root_len, upper_path, upper_type); + } +} + +void print_modified(const char *lower_path, const size_t lower_root_len, mode_t lower_type, const char *upper_path, bool identical) { + if (brief) { + if (!identical) { // brief format does not print permission difference + printf("%s %s and %s differ\n", ftype_name_plural(lower_type), lower_path, upper_path); + } + } else { + printf("Modified: %s%s\n", &lower_path[lower_root_len], TRAILING_SLASH(lower_type)); + } +} + +int list_deleted_files(const char *lower_path, size_t lower_root_len, mode_t upper_type) { // This WORKS with files and itself is listed. However, prefixs are WRONG! + // brief format needs to print only first level deleted children under opaque dir + bool children = (brief && (upper_type == S_IFDIR)); + if (!verbose && !children) { + if (!brief || upper_type == S_IFCHR) { // dir replaced already printed by print_replaced() + print_removed(lower_path, lower_root_len, S_IFDIR); + } + return 0; + } + FTSENT *cur; + char *paths[2] = {(char *) lower_path, NULL }; + FTS *ftsp = fts_open(paths, FTS_NOCHDIR | FTS_PHYSICAL, NULL); + if (ftsp == NULL) { return -1; } + int return_val = 0; + while (((cur = fts_read(ftsp)) != NULL) && (return_val == 0)) { + switch (cur->fts_info) { + case FTS_D: + // brief format does not need to print deleted grand children under opaque dir + if (children && cur->fts_level > 0) { + print_removed(cur->fts_path, lower_root_len, S_IFDIR); + fts_set(ftsp, cur, FTS_SKIP); + } + break; // do nothing + case FTS_DP: + // brief format does not need to print deleted dir under opaque dir itself + if (!children) { + print_removed(cur->fts_path, lower_root_len, S_IFDIR); + } + break; + case FTS_F: + print_removed(cur->fts_path, lower_root_len, S_IFREG); + break; + case FTS_SL: + print_removed(cur->fts_path, lower_root_len, S_IFLNK); + break; + case FTS_DEFAULT: + fprintf(stderr, "File %s is a special file (device or pipe). We cannot handle that.\n", cur->fts_path); + return_val = -1; + break; + default: + fprintf(stderr, "Error occured when opening %s.\n", cur->fts_path); + return_val = -1; + } + } + if (errno) { return_val = -1; } // if no error happened, fts_read will "sets the external variable errno to 0" according to the documentation + return fts_close(ftsp) || return_val; +} + +static int diff_d(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { + bool opaque = false; + bool lower_exist = (lower_status != NULL); + if (lower_exist) { + if (file_type(lower_status) == S_IFDIR) { + if (is_opaquedir(upper_path, &opaque) < 0) { return -1; } + if (opaque) { + if (list_deleted_files(lower_path, lower_root_len, S_IFDIR) < 0) { return -1; } + } else { + if (!permission_identical(lower_status, upper_status)) { + print_modified(lower_path, lower_root_len, S_IFDIR, upper_path, true); + } + return 0; // children must be recursed, and directory itself does not need to be printed + } + } else { // other types of files + print_replaced(lower_path, lower_root_len, file_type(lower_status), upper_path, S_IFDIR); + } + } + if (!(verbose || (brief && opaque))) { // brief format needs to print children of opaque dir + *fts_instr = FTS_SKIP; + } + if (!lower_exist || (!brief && opaque)) { // brief format does not need to print opaque dir itself + print_added(lower_path, lower_root_len, upper_path, S_IFDIR); + } + return 0; +} + +static int diff_f(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { + bool identical; + if (lower_status != NULL) { + switch (file_type(lower_status)) { + case S_IFREG: + if (regular_file_identical(lower_path, lower_status, upper_path, upper_status, &identical) < 0) { + return -1; + } + if (!(identical && permission_identical(lower_status, upper_status))) { + print_modified(lower_path, lower_root_len, S_IFREG, upper_path, identical); + } + return 0; + case S_IFDIR: + if (list_deleted_files(lower_path, lower_root_len, S_IFREG) < 0) { return -1; } + /* fallthrough */ + case S_IFLNK: + print_replaced(lower_path, lower_root_len, file_type(lower_status), upper_path, S_IFREG); + return 0; + default: + fprintf(stderr, "File %s is a special file (device or pipe). We cannot handle that.\n", lower_path); + return -1; + } + } + print_added(lower_path, lower_root_len, upper_path, S_IFREG); + return 0; +} + +static int diff_sl(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { + bool identical; + if (lower_status != NULL) { + switch (file_type(lower_status)) { + case S_IFDIR: + if (list_deleted_files(lower_path, lower_root_len, S_IFLNK) < 0) { return -1; } + /* fallthrough */ + case S_IFREG: + print_replaced(lower_path, lower_root_len, file_type(lower_status), upper_path, S_IFLNK); + return 0; + case S_IFLNK: + if (symbolic_link_identical(lower_path, upper_path, &identical) < 0) { + return -1; + } + if (!(identical && permission_identical(lower_status, upper_status))) { + print_modified(lower_path, lower_root_len, S_IFLNK, upper_path, identical); + } + return 0; + default: + fprintf(stderr, "File %s is a special file (device or pipe). We cannot handle that.\n", lower_path); + return -1; + } + } + print_added(lower_path, lower_root_len, upper_path, S_IFLNK); + return 0; +} + +static int diff_whiteout(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { + if (lower_status != NULL) { + if (file_type(lower_status) == S_IFDIR) { + if (list_deleted_files(lower_path, lower_root_len, S_IFCHR) < 0) { return -1; } + } else { + print_removed(lower_path, lower_root_len, file_type(lower_status)); + } + } // else: whiteouting a nonexistent file? must be an error. but we ignore that :) + return 0; +} + +static int merge_d(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { + bool redirect; + if (is_redirect(upper_path, &redirect) < 0) { return -1; } + // merging redirects is not supported, we must abort merge so redirected lower (under whiteout) won't be deleted + // upper_path may be hiding the directory in lower_path, but there may be another redirect upper pointing at it + if (redirect) { + fprintf(stderr, "Found redirect on %s. Merging redirect is not supported - Abort.\n", upper_path); + return -1; + } + if (lower_status != NULL) { + if (file_type(lower_status) == S_IFDIR) { + bool opaque = false; + if (is_opaquedir(upper_path, &opaque) < 0) { return -1; } + if (opaque) { + if (command(script_stream, "rm -r %L", lower_path) < 0) { return -1; }; + } else { + if (!permission_identical(lower_status, upper_status)) { + command(script_stream, "chmod --reference %U %L", upper_path, lower_path); + } + return 0; // children must be recursed, and directory itself does not need to be printed + } + } else { + command(script_stream, "rm %L", lower_path); + } + } + *fts_instr = FTS_SKIP; + return command(script_stream, "mv -T %U %L", upper_path, lower_path); +} + +static int merge_dp(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { + if (lower_status != NULL) { + if (file_type(lower_status) == S_IFDIR) { + bool opaque = false; + if (is_opaquedir(upper_path, &opaque) < 0) { return -1; } + if (!opaque) { // delete the directory: it should be empty already + return command(script_stream, "rmdir %U", upper_path); + } + } + } + return 0; +} + +static int merge_f(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { + bool metacopy, redirect; + if (is_metacopy(upper_path, &metacopy) < 0) { return -1; } + if (is_redirect(upper_path, &redirect) < 0) { return -1; } + // merging metacopy is not supported, we must abort merge so lower data won't be deleted + if (metacopy || redirect) { + fprintf(stderr, "Found metacopy/redirect on %s. Merging metacopy/redirect is not supported - Abort.\n", upper_path); + return -1; + } + return command(script_stream, "rm -rf %L", lower_path) || command(script_stream, "mv -T %U %L", upper_path, lower_path); +} + +static int merge_sl(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { + return command(script_stream, "rm -rf %L", lower_path) || command(script_stream, "mv -T %U %L", upper_path, lower_path); +} + +static int merge_whiteout(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { + return command(script_stream, "rm -r %L", lower_path) || command(script_stream, "rm %U", upper_path); +} + +typedef int (*TRAVERSE_CALLBACK)(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr); + +int traverse(const char *lower_root, const char *upper_root, FILE* script_stream, TRAVERSE_CALLBACK callback_d, TRAVERSE_CALLBACK callback_dp, TRAVERSE_CALLBACK callback_f, TRAVERSE_CALLBACK callback_sl, TRAVERSE_CALLBACK callback_whiteout) { // returns 0 on success + FTSENT *cur; + char *paths[2] = {(char *) upper_root, NULL }; + char lower_path[PATH_MAX]; + strcpy(lower_path, lower_root); + size_t upper_root_len = strlen(upper_root); + size_t lower_root_len = strlen(lower_root); + FTS *ftsp = fts_open(paths, FTS_NOCHDIR | FTS_PHYSICAL, NULL); + if (ftsp == NULL) { return -1; } + int return_val = 0; + while ((return_val == 0) && ((cur = fts_read(ftsp)) != NULL)) { + TRAVERSE_CALLBACK callback = NULL; + switch (cur->fts_info) { + case FTS_D: + callback = callback_d; + break; + case FTS_DP: + callback = callback_dp; + break; + case FTS_F: + callback = callback_f; + break; + case FTS_SL: + callback = callback_sl; + break; + case FTS_DEFAULT: + if (is_whiteout(cur->fts_statp)) { + callback = callback_whiteout; + } else { + return_val = -1; + fprintf(stderr, "File %s is a special file (device or pipe). We cannot handle that.\n", cur->fts_path); + } + break; + default: + return_val = -1; + fprintf(stderr, "Error occured when opening %s.\n", cur->fts_path); + } + if (callback != NULL) { + int fts_instr = 0; + struct stat lower_status; + bool lower_exist = true; + strcpy(&lower_path[lower_root_len], &(cur->fts_path[upper_root_len])); + if (lstat(lower_path, &lower_status) != 0) { + if (errno == ENOENT || errno == ENOTDIR) { // the corresponding lower file (or its ancestor) does not exist at all + lower_exist = false; + } else { // stat failed for some unknown reason + fprintf(stderr, "Failed to stat %s.\n", lower_path); + return_val = -1; + break; // do not call callback in this case + } + } + return_val = callback(lower_path, cur->fts_path, lower_root_len, lower_exist ? &lower_status : NULL, cur->fts_statp, script_stream, &fts_instr); // return_val must previously be 0 + if (fts_instr) { + fts_set(ftsp, cur, fts_instr); + } + } + } + if (errno) { return_val = -1; } // if no error happened, fts_read will "sets the external variable errno to 0" according to the documentation + return fts_close(ftsp) || return_val; +} + +static int deref_d(const char *mnt_path, const char* upper_path, const size_t mnt_root_len, const struct stat *mnt_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { + bool redirect; + if (is_redirect(upper_path, &redirect) < 0) { return -1; } + if (!redirect) { return 0; } + *fts_instr = FTS_SKIP; + return command(script_stream, "rm -rf %U", upper_path) || command(script_stream, "cp -a %M %U", mnt_path, upper_path); +} + +static int deref_f(const char *mnt_path, const char* upper_path, const size_t mnt_root_len, const struct stat *mnt_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { + bool metacopy; + if (is_metacopy(upper_path, &metacopy) < 0) { return -1; } + if (!metacopy) { return 0; } + return command(script_stream, "rm -r %U", upper_path) || command(script_stream, "cp -a %M %U", mnt_path, upper_path); +} + +int vacuum(const char* lowerdir, const char* upperdir, FILE* script_stream) { + return traverse(lowerdir, upperdir, script_stream, vacuum_d, vacuum_dp, vacuum_f, vacuum_sl, NULL); +} + +int diff(const char* lowerdir, const char* upperdir) { + return traverse(lowerdir, upperdir, NULL, diff_d, NULL, diff_f, diff_sl, diff_whiteout); +} + +int merge(const char* lowerdir, const char* upperdir, FILE* script_stream) { + return traverse(lowerdir, upperdir, script_stream, merge_d, merge_dp, merge_f, merge_sl, merge_whiteout); +} + +int deref(const char* mountdir, const char* upperdir, FILE* script_stream) { + return traverse(mountdir, upperdir, script_stream, deref_d, NULL, deref_f, NULL, NULL); +} diff --git a/overlayfs-tools/logic.h b/overlayfs-tools/logic.h new file mode 100755 index 0000000..374e517 --- /dev/null +++ b/overlayfs-tools/logic.h @@ -0,0 +1,37 @@ +/* + * logic.h / logic.c + * + * the logic for the three feature functions + */ + +#ifndef OVERLAYFS_TOOLS_LOGIC_H +#define OVERLAYFS_TOOLS_LOGIC_H + +#include + +extern bool verbose; +extern bool brief; + +/* + * feature function. will take very long time to complete. returns 0 on success + */ +int vacuum(const char* lowerdir, const char* upperdir, FILE* script_stream); + +/* + * feature function. will take very long time to complete. returns 0 on success + */ +int diff(const char* lowerdir, const char* upperdir); + +/* + * feature function. will take very long time to complete. returns 0 on success + */ +int merge(const char* lowerdir, const char* upperdir, FILE* script_stream); + +/* + * Unfold metacopy and redirect upper. + * + * mountdir is required and lowerdir is irrelevant. + */ +int deref(const char* mountdir, const char* upperdir, FILE* script_stream); + +#endif //OVERLAYFS_TOOLS_LOGIC_H diff --git a/overlayfs-tools/main.c b/overlayfs-tools/main.c new file mode 100755 index 0000000..bea469f --- /dev/null +++ b/overlayfs-tools/main.c @@ -0,0 +1,266 @@ +/* + * main.c + * + * the command line user interface + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef _SYS_STAT_H + #include +#endif +#include "logic.h" +#include "sh.h" + +#define STRING_BUFFER_SIZE PATH_MAX * 2 + +// currently, brief and verbose are mutually exclusive +bool verbose; +bool brief; +bool yes; + +void print_help(const char *program) { + printf("Usage: %s command options\n", program); + puts(""); + puts("Commands:"); + puts(" vacuum - remove duplicated files in upperdir where copy_up is done but the file is not actually modified"); + puts(" diff - show the list of actually changed files"); + puts(" merge - merge all changes from upperdir to lowerdir, and clear upperdir"); + puts(" deref - copy changes from upperdir to a new upperdir unfolding redirect and metacopy"); + puts(""); + puts("Options:"); + puts(" -l, --lowerdir=LOWERDIR the lowerdir of OverlayFS (required)"); + puts(" -u, --upperdir=UPPERDIR the upperdir of OverlayFS (required)"); + puts(" -m, --mountdir=MOUNTDIR the mountdir of OverlayFS (optional)"); + puts(" -L, --lowernew=LOWERNEW the lowerdir of new OverlayFS (optional)"); + puts(" -U, --uppernew=UPPERNEW the upperdir of new OverlayFS (optional)"); + puts(" -y --yes don't prompt if OverlayFS is still mounted (optional)"); + puts(" -v, --verbose with diff action only: when a directory only exists in one version, still list every file of the directory"); + puts(" -b, --brief with diff action only: conform to output of diff --brief --recursive --no-dereference"); + puts(" -h, --help show this help text"); + puts(""); + puts("See https://github.com/kmxz/overlayfs-tools/ for warnings and more information."); +} + +bool starts_with(const char *haystack, const char* needle) { + return strncmp(needle, haystack, strlen(needle)) == 0; +} + +bool is_mounted(const char *lower, const char *upper) { + FILE *f = fopen("/proc/mounts", "r"); + if (!f) { + fprintf(stderr, "Cannot read /proc/mounts to test whether OverlayFS is mounted.\n"); + return true; + } + char buf[STRING_BUFFER_SIZE]; + while (fgets(buf, STRING_BUFFER_SIZE, f)) { + if (!starts_with(buf, "overlay")) { + continue; + } + if (strlen(buf) == STRING_BUFFER_SIZE) { + fprintf(stderr, "OverlayFS line in /proc/mounts is too long.\n"); + return true; + } + char *m_lower = strstr(buf, "lowerdir="); + char *m_upper = strstr(buf, "upperdir="); + if (m_lower == NULL || m_upper == NULL) { + fprintf(stderr, "Cannot extract information from OverlayFS line in /proc/mounts.\n"); + return true; + } + m_lower = &(m_lower[strlen("lowerdir=")]); + m_upper = &(m_upper[strlen("upperdir=")]); + if (!(strncmp(lower, m_lower, strlen(lower)) && strncmp(upper, m_upper, strlen(upper)))) { + printf("The OverlayFS involved is still mounted.\n"); + return true; + } + } + return false; +} + +bool check_mounted(const char *lower, const char *upper) { + if (is_mounted(lower, upper) && !yes) { + printf("It is strongly recommended to unmount OverlayFS first. Still continue (not recommended)?: \n"); + int r = getchar(); + if (r != 'Y' && r != 'y') { + return true; + } + } + return false; +} + +bool directory_exists(const char *path) { + struct stat sb; + if (lstat(path, &sb) != 0) { return false; } + return (sb.st_mode & S_IFMT) == S_IFDIR; +} + +bool directory_create(const char *name, const char *path) { + if (mkdir(path, 0755) == 0 || errno == EEXIST) { return true; } + fprintf(stderr, "%s directory '%s' does not exist and cannot be created.\n", name, path); + exit(EXIT_FAILURE); +} + +bool real_check_xattr_trusted(const char *tmp_path, int tmp_file) { + int ret = fsetxattr(tmp_file, "trusted.overlay.test", "naive", 5, 0); + close(tmp_file); + if (ret) { return false; } + char verify_buffer[10]; + if (getxattr(tmp_path, "trusted.overlay.test", verify_buffer, 10) != 5) { return false; } + return !strncmp(verify_buffer, "naive", 5); +} + +bool check_xattr_trusted(const char *upper) { + char tmp_path[PATH_MAX]; + strcpy(tmp_path, upper); + strcat(tmp_path, "/.xattr_test_XXXXXX.tmp"); + int tmp_file = mkstemps(tmp_path, 4); + if (tmp_file < 0) { return false; } + bool ret = real_check_xattr_trusted(tmp_path, tmp_file); + unlink(tmp_path); + return ret; +} + +int main(int argc, char *argv[]) { + + char *lower = NULL; + char *upper = NULL; + char *dir, *mnt = NULL; + + static struct option long_options[] = { + { "lowerdir", required_argument, 0, 'l' }, + { "upperdir", required_argument, 0, 'u' }, + { "mountdir", required_argument, 0, 'm' }, + { "lowernew", required_argument, 0, 'L' }, + { "uppernew", required_argument, 0, 'U' }, + { "yes", no_argument , 0, 'y' }, + { "help", no_argument , 0, 'h' }, + { "verbose", no_argument , 0, 'v' }, + { "brief", no_argument , 0, 'b' }, + { 0, 0, 0, 0 } + }; + + int opt = 0; + int long_index = 0; + while ((opt = getopt_long_only(argc, argv, "l:u:m:L:U:yhvb", long_options, &long_index)) != -1) { + switch (opt) { + case 'l': + lower = realpath(optarg, NULL); + if (lower) { vars[LOWERDIR] = lower; } + break; + case 'u': + upper = realpath(optarg, NULL); + if (upper) { vars[UPPERDIR] = upper; } + break; + case 'm': + mnt = realpath(optarg, NULL); + if (mnt) { vars[MOUNTDIR] = mnt; } + break; + case 'L': + directory_create("New lowerdir", optarg); + dir = realpath(optarg, NULL); + if (dir) { vars[LOWERNEW] = dir; } + break; + case 'U': + directory_create("New upperdir", optarg); + dir = realpath(optarg, NULL); + if (dir) { vars[UPPERNEW] = dir; } + break; + case 'y': + yes = true; + break; + case 'h': + print_help(argv[0]); + return EXIT_SUCCESS; + case 'v': + verbose = true; + brief = false; + break; + case 'b': + verbose = false; + brief = true; + break; + default: + fprintf(stderr, "Option %c is not supported.\n", opt); + goto see_help; + } + } + + if (!lower) { + fprintf(stderr, "Lower directory not specified.\n"); + goto see_help; + } + if (!directory_exists(lower)) { + fprintf(stderr, "Lower directory cannot be opened.\n"); + goto see_help; + } + if (!upper) { + fprintf(stderr, "Upper directory not specified.\n"); + goto see_help; + } + if (!directory_exists(upper)) { + fprintf(stderr, "Upper directory cannot be opened.\n"); + goto see_help; + } + if (!check_xattr_trusted(upper)) { + fprintf(stderr, "The program cannot write trusted.* xattr. Try run again as root.\n"); + return EXIT_FAILURE; + } + // Relax check for mounted overlay if we are not going to modify lowerdir/upperdir + if ((!vars[LOWERNEW] || !vars[UPPERNEW]) && check_mounted(lower, upper)) { + return EXIT_FAILURE; + } + + if (optind == argc - 1) { + int out; + char filename_template[] = "overlay-tools-XXXXXX.sh"; + FILE *script = NULL; + if (strcmp(argv[optind], "diff") == 0) { + out = diff(lower, upper); + } else if (strcmp(argv[optind], "vacuum") == 0) { + script = create_shell_script(filename_template); + if (script == NULL) { fprintf(stderr, "Script file cannot be created.\n"); return EXIT_FAILURE; } + out = vacuum(lower, upper, script); + } else if (strcmp(argv[optind], "merge") == 0) { + script = create_shell_script(filename_template); + if (script == NULL) { fprintf(stderr, "Script file cannot be created.\n"); return EXIT_FAILURE; } + out = merge(lower, upper, script); + } else if (strcmp(argv[optind], "deref") == 0) { + if (!mnt || !vars[UPPERNEW]) { fprintf(stderr, "'deref' command requires --uppernew and --mountdir.\n"); return EXIT_FAILURE; } + if (!directory_exists(mnt)) { + fprintf(stderr, "OverlayFS mount directory cannot be opened.\n"); + goto see_help; + } + script = create_shell_script(filename_template); + if (script == NULL) { fprintf(stderr, "Script file cannot be created.\n"); return EXIT_FAILURE; } + out = deref(mnt, upper, script); + } else { + fprintf(stderr, "Action not supported.\n"); + goto see_help; + } + if (script != NULL) { + printf("The script %s is created. Run the script to do the actual work please. Remember to run it when the OverlayFS is not mounted.\n", filename_template); + fclose(script); + } + if (out) { + fprintf(stderr, "Action aborted due to fatal error.\n"); + return EXIT_FAILURE; + } + return EXIT_SUCCESS; + } + + fprintf(stderr, "Please specify one action.\n"); + +see_help: + fprintf(stderr, "Try '%s --help' for more information.\n", argv[0]); + return EXIT_FAILURE; + +} diff --git a/overlayfs-tools/makefile b/overlayfs-tools/makefile new file mode 100755 index 0000000..963cc99 --- /dev/null +++ b/overlayfs-tools/makefile @@ -0,0 +1,23 @@ +CC = gcc +CFLAGS = -Wall -std=c99 +LDFLAGS = -lm +ifneq (,$(wildcard /etc/alpine-release)) + LDFLAGS += -lfts +endif + +all: overlayfs-tools + +overlayfs-tools: main.o logic.o sh.o + $(CC) main.o logic.o sh.o -o overlayfs-tools $(LDFLAGS) + +main.o: main.c logic.h + $(CC) $(CFLAGS) -c main.c + +logic.o: logic.c logic.h sh.h + $(CC) $(CFLAGS) -c logic.c + +sh.o: sh.c sh.h + $(CC) $(CFLAGS) -c sh.c + +clean: + $(RM) main.o logic.o sh.o overlayfs-tools diff --git a/overlayfs-tools/sh.c b/overlayfs-tools/sh.c new file mode 100755 index 0000000..98565ac --- /dev/null +++ b/overlayfs-tools/sh.c @@ -0,0 +1,98 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include "sh.h" + +char * vars[NUM_VARS]; +const char * var_names[NUM_VARS] = { + "LOWERDIR", + "UPPERDIR", + "MOUNTDIR", + "LOWERNEW", + "UPPERNEW", +}; + +int quote(const char *filename, FILE *output); + +FILE* create_shell_script(char *tmp_path_buffer) { + int tmp_file = mkstemps(tmp_path_buffer, 3); // the 3 is for suffix length (".sh") + if (tmp_file < 0) { return NULL; } + fchmod(tmp_file, S_IRWXU); // chmod to 0700 + FILE* f = fdopen(tmp_file, "w"); + if (f == NULL) { return NULL; } + fprintf(f, "#!/usr/bin/env bash\n"); + fprintf(f, "set -x\n"); + time_t rawtime; + time (&rawtime); + fprintf(f, "# This shell script is generated by overlayfs-tools on %s\n", ctime (&rawtime)); + for (int i=0; i < NUM_VARS; i++) { + if (vars[i]) { + fprintf(f, "%s=", var_names[i]); + if (quote(vars[i], f) < 0) { return NULL; } + if (fputc('\n', f) == EOF) { return NULL; } + } + } + // Non-empty *NEW vars make a backup copy and override *DIR vars + if (vars[LOWERNEW]) { + fprintf(f, "rm -rf \"$LOWERNEW\"\n"); + fprintf(f, "cp -a \"$LOWERDIR\" \"$LOWERNEW\"\n"); + fprintf(f, "LOWERDIR="); + if (quote(vars[LOWERNEW], f) < 0) { return NULL; } + if (fputc('\n', f) == EOF) { return NULL; } + } + if (vars[UPPERNEW]) { + fprintf(f, "rm -rf \"$UPPERNEW\"\n"); + fprintf(f, "cp -a \"$UPPERDIR\" \"$UPPERNEW\"\n"); + fprintf(f, "UPPERDIR="); + if (quote(vars[UPPERNEW], f) < 0) { return NULL; } + if (fputc('\n', f) == EOF) { return NULL; } + } + return f; +} + +int quote(const char *filename, FILE *output) { + if (fputc('\'', output) == EOF) { return -1; } + for (const char *s = filename; *s != '\0'; s++) { + if (*s == '\'') { + if (fprintf(output, "'\"'\"'") < 0) { return -1; } + } else { + if (fputc(*s, output) == EOF) { return -1; } + } + } + if (fputc('\'', output) == EOF) { return -1; } + return 0; +} + +int substitue(char what, const char *filename, FILE *output) { + int i; + for (i=0; i < NUM_VARS; i++) + if (vars[i] && var_names[i][0] == what) + break; + if (i == NUM_VARS) { return -1; } + // filename prefix must match the var value + int prefix = strlen(vars[i]); + if (strncmp(filename, vars[i], prefix)) { return -1; } + filename += prefix; + fprintf(output, "\"$%s\"", var_names[i]); + return quote(filename, output); +} + +int command(FILE *output, const char *command_format, ...) { + va_list arg; + va_start(arg, command_format); + for (size_t i = 0; command_format[i] != '\0'; i++) { + if (command_format[i] == '%') { + const char *s = va_arg(arg, char *); + if (substitue(command_format[++i], s, output) < 0) { return -1; } + } else { + if (fputc(command_format[i], output) == EOF) { return -1; } + } + } + va_end(arg); + if (fputc('\n', output) == EOF) { return -1; } + return 0; +} diff --git a/overlayfs-tools/sh.h b/overlayfs-tools/sh.h new file mode 100755 index 0000000..19d9bdb --- /dev/null +++ b/overlayfs-tools/sh.h @@ -0,0 +1,20 @@ +#ifndef OVERLAYFS_TOOLS_SH_H +#define OVERLAYFS_TOOLS_SH_H + +enum { + LOWERDIR, + UPPERDIR, + MOUNTDIR, + LOWERNEW, + UPPERNEW, + NUM_VARS +}; + +extern const char *var_names[NUM_VARS]; +extern char *vars[NUM_VARS]; + +FILE* create_shell_script(char *tmp_path_buffer); + +int command(FILE *output, const char *command_format, ...); + +#endif //OVERLAYFS_TOOLS_SH_H From 158cc38424fe3e8a12911712e9ead56ea94345ef Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Tue, 25 Apr 2023 16:46:45 +0530 Subject: [PATCH 051/121] Update readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 823f357..f9cc63f 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ The `init-blend` file in this repository uses a few lines (the sections have bee I would also like to thank Luca Di Maio from Distrobox for NVIDIA driver support in containers. +`overlayfs-tools` has been taken from https://github.com/ecdye/zram-config, which itself forked https://github.com/kmxz/overlayfs-tools. + Aside from these lines, all the other code in this repository has been written by me (rs2009). `blend-settings` is based on [Modren](https://github.com/RudraSwat/modren), a software store I (rs2009) had written long ago, and is licensed under the same license as the rest of the code in this repository, [the GPL-3.0 license](https://github.com/blend-os/blend/blob/main/LICENSE). ## Usage From 46eb49a898d06b01f21a089e57edee8e6e6ad2d5 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Tue, 25 Apr 2023 17:43:23 +0530 Subject: [PATCH 052/121] Fix systemd errors in Ubuntu containers --- init-blend | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/init-blend b/init-blend index 71c2b92..c088645 100755 --- a/init-blend +++ b/init-blend @@ -255,11 +255,22 @@ if [ -d "/usr/lib/rpm/" ]; then net_mounts=${net_mounts%?} echo "%_netsharedpath ${net_mounts}" > /usr/lib/rpm/macros.d/macros.blend elif [ -d "/etc/dpkg/" ]; then - mkdir -p /etc/dpkg/dpkg.cfg.d + mkdir -p /etc/dpkg/dpkg.cfg.d /etc/apt/apt.conf.d + echo -n > /etc/dpkg/dpkg.cfg.d/00_blend for net_mount in ${HOST_MOUNTS_RO} ${HOST_MOUNTS} '/etc/hosts' '/etc/resolv.conf' '/etc/localtime'; do printf "path-exclude %s/*\n" "${net_mount}" >> /etc/dpkg/dpkg.cfg.d/00_blend done + + echo -n > /etc/apt/apt.conf.d/00_blend + for init_mount in ${init_ro_mounts}; do + printf 'DPkg::Pre-Invoke {"if findmnt %s >/dev/null; then umount -l %s; fi";};\n' \ + "${init_mount}" "${init_mount}" >> /etc/apt/apt.conf.d/00_blend + + printf 'DPkg::Post-Invoke {"if [ -e /run/host/%s ] || [ -e /run/host/$(readlink -fm /run/host/%s) ]; then mount --rbind $(readlink -fm /run/host/%s) %s 2>/dev/null || mount --rbind /run/host/$(readlink -fm /run/host/%s) %s; fi";};\n' \ + "${init_mount}" "${init_mount}" "${init_mount}" "${init_mount}" "${init_mount}" "${init_mount}" >> /etc/apt/apt.conf.d/00_blend + done + ### Section END elif [ -d "/usr/share/libalpm/scripts" ]; then echo "#!/bin/sh" > /usr/share/libalpm/scripts/00_blend_pre_hook.sh From 92115c2294142a23130011ecb98405673d32cdd9 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Tue, 25 Apr 2023 21:11:01 +0530 Subject: [PATCH 053/121] Fix PKGBUILD, add overlayfs-tools --- PKGBUILD | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 25e33d9..3b87f48 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -2,14 +2,14 @@ pkgbase=blend-git pkgname=('blend-git' 'blend-settings-git') -pkgver=r35.1a18d5f +pkgver=r42.158cc38 pkgrel=1 _electronversion=22 pkgdesc="A package manager for blendOS" arch=('x86_64' 'i686') url="https://github.com/blend-os/blend" license=('GPL3') -makedepends=("electron${_electronversion}" 'git' 'npm') +makedepends=("electron${_electronversion}" 'git' 'npm' 'base-devel') source=('git+https://github.com/blend-os/blend.git' 'blend-settings.desktop' 'blend-settings' @@ -38,6 +38,7 @@ build() { npm run icons npm run pack -- -c.electronDist=${electronDist} \ -c.electronVersion=${electronVer} --publish never + cd ../overlayfs-tools; make } package_blend-git() { @@ -50,13 +51,12 @@ package_blend-git() { "${pkgname%-git}" \ "init-${pkgname%-git}" \ "host-${pkgname%-git}" \ - "${pkgname%-git}-system" \ "${pkgname%-git}-files" \ -t "${pkgdir}"/usr/bin/ + install -Dm755 "overlayfs-tools/overlayfs-tools" -t \ + "${pkgdir}/usr/bin/" install -Dm644 ../"${pkgname%-git}.sh" -t \ "${pkgdir}"/etc/profile.d/ - install -Dm644 "${pkgname%-git}-system.service" -t \ - "${pkgdir}"/usr/lib/systemd/system/ install -Dm644 "${pkgname%-git}-files.service" -t \ "${pkgdir}"/usr/lib/systemd/user/ install -Dm644 "${pkgname%-git}.hook" \ From 956e8eb1b556d6f132a6593569f542f5ef1b2a77 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Wed, 26 Apr 2023 10:11:52 +0530 Subject: [PATCH 054/121] Handle symlinks too from containers --- blend-files | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/blend-files b/blend-files index aecc6b6..fc9a7f4 100755 --- a/blend-files +++ b/blend-files @@ -65,7 +65,9 @@ def create_container_binaries(): for c in _list: c = c.strip() for i in con_get_output(c, '''find /usr/bin -type f -printf "%P\n" 2>/dev/null; - find /usr/local/bin -type f -printf "%P\n" 2>/dev/null;''').split('\n'): + find -L /usr/bin -xtype l -type f -printf "%P\n" 2>/dev/null; + find /usr/local/bin -type f -printf "%P\n" 2>/dev/null; + find -L /usr/local/bin -xtype l -type f -printf "%P\n" 2>/dev/null;''').split('\n'): i = i.strip() os.makedirs(os.path.expanduser( f'~/.local/bin/blend_{c}'), exist_ok=True) From eea89f26cc2e8bbda9f7140503eb64be875d92e2 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Tue, 2 May 2023 23:54:00 +0530 Subject: [PATCH 055/121] Rework auto-availability of binaries --- blend-settings/main.js | 3 +- blend-settings/src/internal/js/android.js | 4 +- blend-settings/src/internal/js/containers.js | 197 +++++++++++-------- blend-settings/src/pages/containers.html | 56 ++++-- blend-settings/src/pages/terminal.html | 1 + 5 files changed, 154 insertions(+), 107 deletions(-) diff --git a/blend-settings/main.js b/blend-settings/main.js index 6f721e8..7c13b8a 100644 --- a/blend-settings/main.js +++ b/blend-settings/main.js @@ -24,7 +24,7 @@ function createWindow() { autoHideMenuBar: true }) - mainWindow.setMenu(null) + // mainWindow.setMenu(null) mainWindow.loadFile('src/index.html') } @@ -75,6 +75,7 @@ function loadTerminalWindow(title, cmd) { terminalWindow.show() ipcMain.removeAllListeners('terminal.reset') + ipcMain.removeAllListeners('terminal.resize') ipcMain.removeAllListeners('terminal.keystroke') ipcMain.removeAllListeners('terminal.incomingData') ipcMain.removeAllListeners('title') diff --git a/blend-settings/src/internal/js/android.js b/blend-settings/src/internal/js/android.js index ecab68a..9760848 100644 --- a/blend-settings/src/internal/js/android.js +++ b/blend-settings/src/internal/js/android.js @@ -7,9 +7,9 @@ function init_waydroid() { require('child_process').spawnSync('pkexec', ['systemctl', 'enable', '--now', 'waydroid-container']) require('child_process').spawn('sh', ['-c', 'waydroid session start & disown']) setTimeout(() => { - require('child_process').spawnSync('waydroid', ['prop', 'set', 'persist.waydroid.multi_windows', 'true']) + require('child_process').spawnSync('sh', ['-c', 'echo "persist.waydroid.multi_windows=true" | pkexec tee -a /var/lib/waydroid/waydroid_base.prop']) require('child_process').spawnSync('pkexec', ['waydroid', 'shell', 'pm', 'disable', 'com.android.inputmethod.latin']) - if (require('child_process').spawnSync('sh', ['-c', 'LC_ALL=C glxinfo | grep "^OpenGL renderer string: "']).stdout.includes('NVIDIA')) { + if (require('child_process').spawnSync('sh', ['-c', 'LC_ALL=C glxinfo | grep "^OpenGL renderer string: "']).stdout.includes('NVIDIA') || require('child_process').spawnSync('cat', ['/proc/cpuinfo']).stdout.includes('hypervisor')) { require('child_process').spawnSync('sh', ['-c', 'echo "ro.hardware.gralloc=default" | pkexec tee -a /var/lib/waydroid/waydroid.cfg']) require('child_process').spawnSync('sh', ['-c', 'echo "ro.hardware.egl=swiftshader" | pkexec tee -a /var/lib/waydroid/waydroid.cfg']) } diff --git a/blend-settings/src/internal/js/containers.js b/blend-settings/src/internal/js/containers.js index 07dc151..43f6c1f 100644 --- a/blend-settings/src/internal/js/containers.js +++ b/blend-settings/src/internal/js/containers.js @@ -1,21 +1,27 @@ var term function open_container(name) { - ipc.send("create-term", { 'title': `Container: ${name}`, 'cmd': `blend enter -cn ${name}` }); + ipc.send("create-term", { 'title': `Container: ${name}`, 'cmd': `BLEND_NO_CHECK=true blend enter -cn ${name}` }); } -function create_container () { +function create_container() { + $('#inputContainerName').on('input', () => { + $('#inputContainerName').get(0).setCustomValidity('') + $('#inputContainerName').get(0).reportValidity(); + }) + container_name = $('#inputContainerName').val() - if (!(/^[\w\-\.]+$/.test(container_name))) { - $('#inputContainerName').get(0).setCustomValidity('Container name can only contain alphanumeric characters and dashes (no spaces allowed).') + if (!(/^[\w\-]+$/.test(container_name))) { + $('#inputContainerName').get(0).setCustomValidity('Container name may only contain alphanumeric characters and dashes (no spaces allowed).') $('#inputContainerName').get(0).reportValidity(); return } container_distro = $('#inputContainerDistro').val().toLowerCase().replace(' ', '-') - ipc.send("create-term", { 'title': `Creating container: ${container_name}`, - 'cmd': `blend create-container -cn ${container_name} -d ${container_distro} \ - && echo 'created container successfully (exiting automatically in 5 seconds)' \ - || echo 'container creation failed (exiting automatically in 5 seconds)'; + ipc.send("create-term", { + 'title': `Creating container: ${container_name}`, + 'cmd': `blend create-container -cn ${container_name} -d ${container_distro} \ + && echo -e '\nExiting automatically in 5 seconds.' \ + || echo -e '\nContainer creation failed. Exiting automatically in 5 seconds.'; sleep 5` }) $('#inputContainerName').val('') ipc.on('container-created', () => { @@ -23,7 +29,7 @@ function create_container () { }) } -async function remove_container (name) { +async function remove_container(name) { let rm_worker = new Worker( `data:text/javascript, require('child_process').spawnSync('podman', ['stop', '-t', '0', '${name}'], { encoding: 'utf8' }) @@ -37,47 +43,7 @@ async function remove_container (name) { window.worker = new Worker( `data:text/javascript, function list_containers() { - let container_list = require('child_process').spawnSync('podman', ['ps', '-a', '--no-trunc', '--size', '--format', '{{.Names}}'], { encoding: 'utf8' }).stdout.split(/\\r?\\n/).filter(Boolean).reverse(); - if (require('fs').existsSync(require('path').join(require('os').homedir(), '.config/blend/config.yaml'), 'utf8')) { - try { - let fileContents = require('fs').readFileSync(require('path').join(require('os').homedir(), '.config/blend/config.yaml'), 'utf8') - let data = require('js-yaml').load(fileContents); - new_container_list = data['container_order'] - container_list.forEach(container => { - if (!new_container_list.includes(container)) { - new_container_list.push(container) - } - }); - new_container_list = new_container_list.filter(container => container_list.includes(container)) - new_container_list.filter((item, index) => arr.indexOf(item) === index) - data = { - container_order: [...new_container_list], - use_container_bins: [] - }; - contents = require('js-yaml').dump(data) - require('fs').writeFileSync(require('path').join(require('os').homedir(), '.config/blend/config.yaml'), contents, 'utf8') - return new_container_list - } catch (e) { - let data = { - container_order: [...container_list], - use_container_bins: [] - }; - contents = require('js-yaml').dump(data) - require('fs').writeFileSync(require('path').join(require('os').homedir(), '.config/blend/config.yaml'), contents, 'utf8') - - return container_list - } - } else { - let data = { - container_order: [...container_list], - use_container_bins: [] - }; - require('fs').mkdirSync(require('path').join(require('os').homedir(), '.config/blend'), { recursive: true }); - contents = require('js-yaml').dump(data) - require('fs').writeFileSync(require('path').join(require('os').homedir(), '.config/blend/config.yaml'), contents, 'utf8') - - return container_list - } + return require('child_process').spawnSync('podman', ['ps', '-a', '--no-trunc', '--size', '--format', '{{.Names}}'], { encoding: 'utf8' }).stdout.split(/\\r?\\n/).filter(Boolean).reverse(); } function truncateText(text, maxLength=30) { @@ -98,7 +64,7 @@ window.worker = new Worker(
No containers present. -

Create one from below.

+

Create one from above.

@@ -108,25 +74,19 @@ window.worker = new Worker( container_list.forEach(container => { container_list_html += \` -
-
+
+
- - - \${container}
- + - - -
@@ -155,31 +115,98 @@ worker.postMessage('update-list') worker.onmessage = function (event) { window.data = event.data - if (event.data.includes('bi-grip-vertical')) { - $('#container-list').addClass('sortable') - $('#container-list').html(event.data) - } else { - $('#container-list').removeClass('sortable') - $('#container-list').html(event.data) + $('#container-list').html(event.data) +} + +function create_association() { + $('#inputAssociationContainerName').on('input', () => { + $('#inputAssociationContainerName').get(0).setCustomValidity('') + $('#inputAssociationContainerName').get(0).reportValidity(); + }) + + $('#inputAssociationBinaryName').on('input', () => { + $('#inputAssociationBinaryName').get(0).setCustomValidity('') + $('#inputAssociationBinaryName').get(0).reportValidity(); + }) + + if (!(/^[\w\-]+$/.test($('#inputAssociationContainerName').val()))) { + $('#inputAssociationContainerName').get(0).setCustomValidity('Container name may only contain alphanumeric characters and dashes (no spaces allowed).') + $('#inputAssociationContainerName').get(0).reportValidity(); + return } - $('.sortable').each((i, e) => { - Sortable.create(e, { - animation: 100, - onEnd: () => { - let container_list = [] - $('.container_name').each((i, e) => { - container_list.push(e.innerText) - }) - let data = { - container_order: [...container_list], - use_container_bins: [] - }; - require('fs').mkdirSync(require('path').join(require('os').homedir(), '.config/blend'), { recursive: true }); - contents = require('js-yaml').dump(data) - require('fs').writeFileSync(require('path').join(require('os').homedir(), '.config/blend/config.yaml'), contents, 'utf8') - } - }); - e.addEventListener("dragstart", e => e.dataTransfer.setDragImage(new Image(), 0, 0), false); + if (!(/^[\w\-]+$/.test($('#inputAssociationBinaryName').val()))) { + $('#inputAssociationBinaryName').get(0).setCustomValidity('Binary name may only contain alphanumeric characters and dashes (no spaces allowed).') + $('#inputAssociationBinaryName').get(0).reportValidity(); + return + } + + if (binary_names.includes($('#inputAssociationBinaryName').val())) { + $('#inputAssociationBinaryName').get(0).setCustomValidity('Association already exists.') + $('#inputAssociationBinaryName').get(0).reportValidity(); + return + } + + fs.appendFile(require('os').homedir() + '/.local/bin/blend_bin/.associations', `${$('#inputAssociationBinaryName').val()}\0${$('#inputAssociationContainerName').val()}\n`, err => { + require('child_process').spawnSync('ln', ['-sf', $('#inputAssociationBinaryName').val() + '.' + $('#inputAssociationContainerName').val(), require('os').homedir() + '/.local/bin/blend_bin/' + $('#inputAssociationBinaryName').val()]) + update_association_list() + $('#inputAssociationContainerName').val('') + $('#inputAssociationBinaryName').val('') }) -} \ No newline at end of file +} + +function remove_association(binary_name) { + require('child_process').spawnSync('bash', ['-c', `sed -i 's/^${binary_name}\\x0//g' ~/.local/bin/blend_bin/.associations`]) + require('child_process').spawnSync('rm', ['-f', require('os').homedir() + '/.local/bin/blend_bin/' + binary_name]) + update_association_list() +} + +var binary_names = [] + +function update_association_list() { + require('fs').readFile(require('os').homedir() + '/.local/bin/blend_bin/.associations', 'utf8', (err, data) => { + let association_list_html = '' + binary_names = [] + + data.split('\n').forEach(line => { + if (line.includes('\0')) { + let binary_name = line.split('\0')[0] + binary_names.push(binary_name) + let container_name = line.split('\0')[1] + console.log(binary_name, container_name) + association_list_html += ` +
+
+
+ ${binary_name} ${container_name} +
+
+ + + + +
+
+
+ ` + } + }) + + if (association_list_html != '') { + $('#association-list').html(association_list_html) + } else { + $('#association-list').html(` +
+
+
+ No associations. +

Add one from above. It's recommended to add associations for each of the supported package managers for easier usage (apt, pacman and dnf).

+
+
+
+ `) + } + }) +} + +update_association_list() \ No newline at end of file diff --git a/blend-settings/src/pages/containers.html b/blend-settings/src/pages/containers.html index 130da17..59eba24 100644 --- a/blend-settings/src/pages/containers.html +++ b/blend-settings/src/pages/containers.html @@ -1,7 +1,28 @@
Containers -

You can install any app from any of the supported distributions (Arch, Fedora, and Ubuntu). Apps you install will appear as regular applications on your system (as well as binaries and package managers). You can override the priority in which common binaries are made available on the system by rearranging (dragging) the containers below to select the priority that should be assigned to each container.

+

Installed apps will appear in the application launcher. Binaries can be executed with the container's name as a suffix. For example, apt -> apt.ubuntu, in a container named 'ubuntu'.

+ +
+
+
+ +
+
+ +
+
+ +
+
+
@@ -17,29 +38,26 @@
- Create new container -

Create a container for each distribution to be able to use their package managers and other binaries directly from a terminal.

-
+ Associations +

You can associate a binary to a container so as to use it without a suffix.

+
- +
+
+ +
- + +
+
+
-
- -
- -
-
-
-
+
+
diff --git a/blend-settings/src/pages/terminal.html b/blend-settings/src/pages/terminal.html index 0f89938..090d0dc 100644 --- a/blend-settings/src/pages/terminal.html +++ b/blend-settings/src/pages/terminal.html @@ -54,6 +54,7 @@ function create_term() { ipc.removeAllListeners('terminal.reset') + ipc.removeAllListeners('terminal.resize') ipc.removeAllListeners('terminal.keystroke') ipc.removeAllListeners('terminal.incomingData') ipc.removeAllListeners('title') From 451ffc266780ca4fc5614871d084978994ea5df8 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Wed, 3 May 2023 00:02:57 +0530 Subject: [PATCH 056/121] Add missing changes --- blend | 196 ++++++++++++++++++++++++++++--------------- blend-files | 235 ++-------------------------------------------------- init-blend | 140 +++++++++++++++++++++++++++++-- 3 files changed, 270 insertions(+), 301 deletions(-) diff --git a/blend b/blend index fff273e..6aee307 100755 --- a/blend +++ b/blend @@ -1,32 +1,38 @@ #!/usr/bin/env python3 # Copyright (C) 2023 Rudra Saraswat -# +# # This file is part of blend. -# +# # blend is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. -# +# # blend is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with blend. If not, see . -import os, sys, getpass, time +import os +import sys +import glob +import time import shutil import socket +import getpass import pexpect import argparse import subprocess __version = '2.0.0' -### Colors +# Colors + + class colors: reset = '\033[0m' bold = '\033[01m' @@ -63,17 +69,22 @@ class colors: cyan = '\033[46m' lightgrey = '\033[47m' -### END +# END + +# Helper functions -### Helper functions def info(msg): - print (colors.bold + colors.fg.cyan + '>> i: ' + colors.reset + colors.bold + msg + colors.reset) + print(colors.bold + colors.fg.cyan + '>> i: ' + + colors.reset + colors.bold + msg + colors.reset) + def error(err): - print (colors.bold + colors.fg.red + '>> e: ' + colors.reset + colors.bold + err + colors.reset) + print(colors.bold + colors.fg.red + '>> e: ' + + colors.reset + colors.bold + err + colors.reset) + +# END -### END distro_map = { 'arch': 'docker.io/library/archlinux', @@ -84,6 +95,7 @@ distro_map = { default_distro = 'arch' + def get_distro(): try: return distro_map[args.distro] @@ -91,9 +103,10 @@ def get_distro(): error(f"{args.distro} isn't supported by blend.") exit() + def list_containers(): _list = subprocess.run(['podman', 'ps', '-a', '--no-trunc', '--size', '--format', - '{{.Names}}:{{.Mounts}}'], stdout=subprocess.PIPE).stdout.decode('utf-8').strip() + '{{.Names}}:{{.Mounts}}'], stdout=subprocess.PIPE).stdout.decode('utf-8').strip() if len(_list) == 0: info('No containers. Create one by installing a package (`blend install hello`), or manually create one (`blend create-container arch`).') else: @@ -103,21 +116,25 @@ def list_containers(): print(f"{colors.bold}{i}.{colors.reset} {container.split(':')[0]}") return False + def check_container(name): _list = subprocess.run(['podman', 'ps', '-a', '--no-trunc', '--size', '--format', - '{{.Names}}:{{.Mounts}}'], stdout=subprocess.PIPE).stdout.decode('utf-8').strip() + '{{.Names}}:{{.Mounts}}'], stdout=subprocess.PIPE).stdout.decode('utf-8').strip() for container in _list.splitlines(keepends=False): if ('blend' in container.split(':')[1] or 'distrobox' in container.split(':')[1]) and name.strip() == container.split(':')[0]: - return True + return True return False -def check_container_status(name): + +def check_container_status(name): return host_get_output("podman inspect --type container " + name + " --format \"{{.State.Status}}\"") -def core_start_container(name): - subprocess.call(['podman', 'start', name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - start_time = time.time() - 1000 # workaround +def core_start_container(name): + subprocess.call(['podman', 'start', name], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + start_time = time.time() - 1000 # workaround if check_container_status(name) != 'running': print('') error('the entry point failed to run; try again later') @@ -125,12 +142,14 @@ def core_start_container(name): subprocess.call(['podman', 'logs', '--since', str(start_time), name]) exit(1) - logproc = pexpect.spawn('podman', args=['logs', '-f', '--since', str(start_time), name], timeout=300) + logproc = pexpect.spawn( + 'podman', args=['logs', '-f', '--since', str(start_time), name], timeout=300) logproc.logfile_read = sys.stdout.buffer logproc.expect('Completed container setup.') logproc.terminate() + def core_create_container(): name = args.container_name distro = args.distro @@ -145,32 +164,39 @@ def core_create_container(): podman_command.extend(['--network', 'host']) podman_command.extend(['--security-opt', 'label=disable']) podman_command.extend(['--user', 'root:root', '--pid', 'host']) - podman_command.extend(['--label', 'manager=blend']) # identify as blend container + # identify as blend container + podman_command.extend(['--label', 'manager=blend']) # Env variables podman_command.extend(['--env', 'HOME=' + os.path.expanduser('~')]) + podman_command.extend(['--env', 'CONTAINER_NAME=' + name]) # Volumes podman_command.extend(['--volume', '/:/run/host:rslave']) podman_command.extend(['--volume', '/tmp:/tmp:rslave']) - podman_command.extend(['--volume', f"{os.path.expanduser('~')}:{os.path.expanduser('~')}:rslave"]) - podman_command.extend(['--volume', f"/run/user/{os.geteuid()}:/run/user/{os.geteuid()}:rslave"]) + podman_command.extend( + ['--volume', f"{os.path.expanduser('~')}:{os.path.expanduser('~')}:rslave"]) + podman_command.extend( + ['--volume', f"/run/user/{os.geteuid()}:/run/user/{os.geteuid()}:rslave"]) # Volumes (config files) podman_command.extend(['--volume', f"/etc/hosts:/etc/hosts:ro"]) podman_command.extend(['--volume', f"/etc/localtime:/etc/localtime:ro"]) - podman_command.extend(['--volume', f"/etc/resolv.conf:/etc/resolv.conf:ro"]) + podman_command.extend( + ['--volume', f"/etc/resolv.conf:/etc/resolv.conf:ro"]) # Volumes (files and tools) podman_command.extend(['--volume', '/usr/bin/init-blend:/usr/bin/init-blend:ro', - '--entrypoint', '/usr/bin/init-blend']) # our entrypoint - podman_command.extend(['--volume', '/usr/bin/host-blend:/usr/bin/host-blend:ro']) # and the tool to run commands on the host + '--entrypoint', '/usr/bin/init-blend']) # our entrypoint + # and the tool to run commands on the host + podman_command.extend( + ['--volume', '/usr/bin/host-blend:/usr/bin/host-blend:ro']) podman_command.extend(['--volume', '/var/log/journal']) podman_command.extend(['--mount', 'type=devpts,destination=/dev/pts', '--userns', 'keep-id', '--annotation', 'run.oci.keep_original_groups=1']) - + podman_command.extend([get_distro()]) # User (for init-blend) @@ -189,17 +215,24 @@ def core_create_container(): core_start_container(name) -core_get_output = lambda cmd: subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode('UTF-8').strip() -host_get_output = lambda cmd: subprocess.run(['bash', '-c', cmd], - stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode('UTF-8').strip() +def core_get_output(cmd): return subprocess.run( + cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode('UTF-8').strip() + + +def host_get_output(cmd): return subprocess.run(['bash', '-c', cmd], + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode('UTF-8').strip() + + +def core_get_retcode(cmd): return subprocess.run(['podman', 'exec', '--user', getpass.getuser(), '-it', args.container_name, 'bash', '-c', cmd], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode -core_get_retcode = lambda cmd: subprocess.run(['podman', 'exec', '--user', getpass.getuser(), '-it', args.container_name, 'bash', '-c', cmd], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode def core_run_container(cmd): - if os.getcwd() == os.path.expanduser('~') or os.getcwd().startswith(os.path.expanduser('~') + '/'): - subprocess.call(['podman', 'exec', '--user', getpass.getuser(), '-w', os.getcwd(), '-it', args.container_name, 'bash', '-c', cmd]) + if os.getcwd() == os.path.expanduser('~') or os.getcwd().startswith(os.path.expanduser('~') + '/'): + subprocess.call(['podman', 'exec', '--user', getpass.getuser(), + '-w', os.getcwd(), '-it', args.container_name, 'bash', '-c', cmd]) + def core_install_pkg(pkg): if args.distro == 'fedora-rawhide': @@ -210,8 +243,10 @@ def core_install_pkg(pkg): elif args.distro == 'arch': if core_get_retcode('[ -f /usr/bin/yay ]') != 0: core_run_container('sudo pacman -Sy') - core_run_container('sudo pacman --noconfirm -Syu --needed git base-devel') - core_run_container('TEMP_DIR="$(mktemp -d)"; cd "${TEMP_DIR}"; git clone https://aur.archlinux.org/yay.git; cd yay; makepkg --noconfirm -si; rm -rf "${TEMP_DIR}"') + core_run_container( + 'sudo pacman --noconfirm -Syu --needed git base-devel') + core_run_container( + 'TEMP_DIR="$(mktemp -d)"; cd "${TEMP_DIR}"; git clone https://aur.archlinux.org/yay.git; cd yay; makepkg --noconfirm -si; rm -rf "${TEMP_DIR}"') core_run_container(f'yay -Sy') if args.noconfirm == True: core_run_container(f'yay --noconfirm -Syu {pkg}') @@ -224,6 +259,7 @@ def core_install_pkg(pkg): else: core_run_container(f'sudo apt-get install {pkg}') + def core_remove_pkg(pkg): if args.distro == 'fedora-rawhide': if args.noconfirm == True: @@ -242,6 +278,7 @@ def core_remove_pkg(pkg): core_run_container(f'sudo apt-get purge {pkg}') core_run_container(f'sudo apt-get autoremove --purge -y {pkg}') + def core_search_pkg(pkg): if args.distro == 'fedora-rawhide': core_run_container(f'dnf search {pkg}') @@ -252,6 +289,7 @@ def core_search_pkg(pkg): core_run_container(f'sudo apt-get update') core_run_container(f'apt-cache search {pkg}') + def core_show_pkg(pkg): if args.distro == 'fedora-rawhide': core_run_container(f'dnf info {pkg}') @@ -262,6 +300,7 @@ def core_show_pkg(pkg): core_run_container(f'sudo apt-get update') core_run_container(f'apt-cache show {pkg}') + def install_blend(): if len(args.pkg) == 0: error('no packages to install') @@ -272,6 +311,7 @@ def install_blend(): core_create_container() core_install_pkg(pkg) + def remove_blend(): if len(args.pkg) == 0: error('no packages to remove') @@ -282,6 +322,7 @@ def remove_blend(): error(f"container {args.container_name} doesn't exist") core_remove_pkg(pkg) + def search_blend(): if len(args.pkg) == 0: error('no packages to search for') @@ -291,6 +332,7 @@ def search_blend(): error(f"container {args.container_name} doesn't exist") core_search_pkg(pkg) + def show_blend(): if len(args.pkg) == 0: error('no packages to show') @@ -301,6 +343,7 @@ def show_blend(): error(f"container {args.container_name} doesn't exist") core_show_pkg(pkg) + def sync_blends(): if args.distro == 'fedora-rawhide': core_run_container(f'dnf makecache') @@ -309,6 +352,7 @@ def sync_blends(): elif args.distro.startswith('ubuntu-'): core_run_container(f'sudo apt-get update') + def update_blends(): if args.distro == 'fedora-rawhide': if args.noconfirm == True: @@ -329,38 +373,42 @@ def update_blends(): else: error(f'distribution {args.distro} is not supported') + def enter_container(): - if os.environ.get('BLEND_NO_CHECK') == None: - if not check_container(args.container_name): - core_create_container() - if check_container_status(args.container_name) != 'running': - core_start_container(args.container_name) - podman_args = [] + podman_args = ['--env', 'LC_ALL=C.UTF-8'] sudo = [] if os.environ.get('SUDO_USER') == None: podman_args = ['--user', getpass.getuser()] else: - sudo = ['sudo', '-u', os.environ.get('SUDO_USER'), f'PATH={os.path.expanduser("~/.local/share/bin/blend_bin")}:/usr/bin'] + sudo = ['sudo', '-u', os.environ.get( + 'SUDO_USER'), f'PATH={os.path.expanduser("~/.local/share/bin/blend_bin")}:/usr/bin'] for name, val in os.environ.items(): - if name not in ['LANG', 'LC_CTYPE', 'PATH', 'HOST', 'HOSTNAME', 'SHELL'] and not name.startswith('_'): + if name not in ['LANG', 'LC_CTYPE', 'LC_ALL', 'PATH', 'HOST', 'HOSTNAME', 'SHELL'] and not name.startswith('_'): podman_args.append('--env') podman_args.append(name + '=' + val) if os.environ.get('BLEND_COMMAND') == None or os.environ.get('BLEND_COMMAND') == '': if args.pkg == []: if os.getcwd() == os.path.expanduser('~') or os.getcwd().startswith(os.path.expanduser('~') + '/'): - exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', os.getcwd(), '-it', args.container_name, 'bash'])) + exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, + '-w', os.getcwd(), '-it', args.container_name, 'bash'])) else: - exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', '/run/host' + os.getcwd(), '-it', args.container_name, 'bash'])) + exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', + '/run/host' + os.getcwd(), '-it', args.container_name, 'bash'])) else: if os.getcwd() == os.path.expanduser('~') or os.getcwd().startswith(os.path.expanduser('~') + '/'): - exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', os.getcwd(), '-it', args.container_name, *args.pkg])) + exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, + '-w', os.getcwd(), '-it', args.container_name, *args.pkg])) else: - exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', '/run/host' + os.getcwd(), '-it', args.container_name, *args.pkg])) + exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', + '/run/host' + os.getcwd(), '-it', args.container_name, *args.pkg])) else: if os.getcwd() == os.path.expanduser('~') or os.getcwd().startswith(os.path.expanduser('~') + '/'): - exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', os.getcwd(), '-it', args.container_name, 'bash', '-c', os.environ.get('BLEND_COMMAND')])) + exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', os.getcwd( + ), '-it', args.container_name, 'bash', '-c', os.environ.get('BLEND_COMMAND')])) else: - exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', '/run/host' + os.getcwd(), '-it', args.container_name, 'bash'])) + exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', + '/run/host' + os.getcwd(), '-it', args.container_name, 'bash'])) + def create_container(): for container in args.pkg: @@ -369,15 +417,22 @@ def create_container(): args.distro = container core_create_container() + def remove_container(): for container in args.pkg: info(f'removing container {container}') - subprocess.run(['podman', 'stop', container], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) - subprocess.run(['podman', 'rm', '-f', container], stdout=subprocess.DEVNULL) + subprocess.run(['podman', 'stop', container], + stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) + subprocess.run(['podman', 'rm', '-f', container], + stdout=subprocess.DEVNULL) + for bin in os.listdir(os.path.expanduser('~/.local/bin/blend_bin')): + if bin.endswith(f'.{container}'): + os.remove(os.path.join(os.path.expanduser('~/.local/bin/blend_bin'), bin)) + def start_containers(): _list = subprocess.run(['podman', 'ps', '-a', '--no-trunc', '--size', '--format', - '{{.Names}}:{{.Mounts}}'], stdout=subprocess.PIPE).stdout.decode('utf-8').strip() + '{{.Names}}:{{.Mounts}}'], stdout=subprocess.PIPE).stdout.decode('utf-8').strip() if len(_list) == 0: info('No containers. Create one by installing a package (`blend install hello`), or manually create one (`blend create-container -d arch`).') for container in _list.splitlines(keepends=False): @@ -385,6 +440,7 @@ def start_containers(): info(f'starting container {container}') subprocess.call(['podman', 'start', container]) + if shutil.which('podman') is None: error("podman isn't installed, which is a hard dep") exit(1) @@ -407,21 +463,27 @@ epilog = f''' parser = argparse.ArgumentParser(description=description, usage=argparse.SUPPRESS, epilog=epilog, formatter_class=argparse.RawTextHelpFormatter) -command_map = { 'enter': enter_container, - 'exec': enter_container, - 'create-container': core_create_container, - 'remove-container': remove_container, - 'list-containers': list_containers, - 'start-containers': start_containers, - 'sync': sync_blends, - 'help': 'help', - 'version': 'version' } -parser.add_argument('command', choices=command_map.keys(), help=argparse.SUPPRESS) -parser.add_argument('pkg', action='store', type=str, nargs='*', help=argparse.SUPPRESS) -parser.add_argument('-cn', '--container-name', action='store', nargs=1, metavar='CONTAINER NAME', help=argparse.SUPPRESS) -parser.add_argument('-y', '--noconfirm', action='store_true', help=argparse.SUPPRESS) -parser.add_argument('-d', '--distro', action='store', nargs=1, metavar='DISTRO', help=argparse.SUPPRESS) -parser.add_argument('-v', '--version', action='version', version=f'%(prog)s {__version}', help=argparse.SUPPRESS) +command_map = {'enter': enter_container, + 'exec': enter_container, + 'create-container': core_create_container, + 'remove-container': remove_container, + 'list-containers': list_containers, + 'start-containers': start_containers, + 'sync': sync_blends, + 'help': 'help', + 'version': 'version'} +parser.add_argument('command', choices=command_map.keys(), + help=argparse.SUPPRESS) +parser.add_argument('pkg', action='store', type=str, + nargs='*', help=argparse.SUPPRESS) +parser.add_argument('-cn', '--container-name', action='store', + nargs=1, metavar='CONTAINER NAME', help=argparse.SUPPRESS) +parser.add_argument('-y', '--noconfirm', + action='store_true', help=argparse.SUPPRESS) +parser.add_argument('-d', '--distro', action='store', nargs=1, + metavar='DISTRO', help=argparse.SUPPRESS) +parser.add_argument('-v', '--version', action='version', + version=f'%(prog)s {__version}', help=argparse.SUPPRESS) if len(sys.argv) == 1: parser.print_help() diff --git a/blend-files b/blend-files index fc9a7f4..57b9244 100755 --- a/blend-files +++ b/blend-files @@ -1,242 +1,19 @@ #!/usr/bin/env python3 import os -import sys -import yaml -import time -import glob -import getpass -import fileinput import subprocess - def get_containers(): - container_list = subprocess.run(['sudo', '-u', user, 'podman', 'ps', '-a', '--no-trunc', '--sort=created', '--format', + return subprocess.run(['podman', 'ps', '-a', '--no-trunc', '--sort=created', '--format', '{{.Names}}'], stdout=subprocess.PIPE).stdout.decode('utf-8').strip().split('\n') - try: - with open(os.path.expanduser('~/.config/blend/config.yaml')) as config_file: - data = yaml.safe_load(config_file) - order = data['container_order'].copy() - order.reverse() - container_list.reverse() - for i in container_list: - if i.strip() not in order: - order.insert(0, i) - for i, o in enumerate(order): - if o not in container_list: - del order[i] - return order - except: - return container_list +if os.path.isdir(os.path.expanduser('~/.local/bin/blend_bin')) and not os.path.isfile(os.path.expanduser('~/.local/bin/blend_bin/.associations')): + subprocess.call(['rm', '-rf', os.path.expanduser('~/.local/bin/blend_bin')], shell=False) + subprocess.call(['bash', '-c', 'rm -f "${HOME}/.local/share/applications/blend;"*'], shell=False) - -def list_use_container_bin(): - try: - with open(os.path.expanduser('~/.config/blend/config.yaml')) as config_file: - data = yaml.safe_load(config_file) - return data['use_container_bins'] - except: - return [] - - -def check_if_present(attr, desktop_str): - for l in desktop_str: - if l.startswith(attr + '='): - return True - return False - - -def which(bin): - results = [] - for dir in os.environ.get('PATH').split(':'): - if os.path.isdir(dir): - if os.path.basename(bin) in os.listdir(dir): - results.append(os.path.join(dir, os.path.basename(bin))) - if results == []: - return None - return results - - -def create_container_binaries(): - _binaries = [] - remove_binaries = [] - - for c in _list: - c = c.strip() - for i in con_get_output(c, '''find /usr/bin -type f -printf "%P\n" 2>/dev/null; - find -L /usr/bin -xtype l -type f -printf "%P\n" 2>/dev/null; - find /usr/local/bin -type f -printf "%P\n" 2>/dev/null; - find -L /usr/local/bin -xtype l -type f -printf "%P\n" 2>/dev/null;''').split('\n'): - i = i.strip() - os.makedirs(os.path.expanduser( - f'~/.local/bin/blend_{c}'), exist_ok=True) - i_present = False - orig_which_out = which(os.path.basename(i)) - which_out = None - if orig_which_out != None: - which_out = orig_which_out.copy() - try: - which_out.remove(os.path.expanduser( - f'~/.local/bin/blend_bin/{os.path.basename(i)}')) - except ValueError: - pass - if which_out == []: - which_out = None - if which_out != None and os.path.basename(i) not in _exceptions: - i_present = True - - if os.path.basename(i) != 'host-spawn' and i != '' and not i_present: - with open(os.path.expanduser(f'~/.local/bin/blend_{c}/{os.path.basename(i)}.tmp'), 'w') as f: - f.write('#!/bin/bash\n') - f.write(f'# blend container: {c};{i}\n') - if os.path.basename(i) in _exceptions: - f.write(f'# EXCEPTION\n') - f.write('[ -f /run/.containerenv ] && { if [[ -e "/usr/bin/' + os.path.basename(i) + '" ]] || [[ -e "/usr/local/bin/' + os.path.basename(i) + '" ]]; then if [[ -e "/usr/bin/' + os.path.basename(i) + '" ]]; then /usr/bin/' + os.path.basename( - i) + ' "$@"; elif [[ -e "/usr/local/bin/' + os.path.basename(i) + '" ]]; then /usr/local/bin/' + os.path.basename(i) + ' "$@"; fi; exit $?; else echo "This command can be accessed from the host, or from the container \'' + c + '\'."; exit 127; fi } || :\n') - f.write( - f'BLEND_ALLOW_ROOT= BLEND_NO_CHECK= blend enter -cn {c} -- {os.path.basename(i)} "$@"\n') - # XXX: make this bit fully atomic - os.chmod(os.path.expanduser( - f'~/.local/bin/blend_{c}/{os.path.basename(i)}.tmp'), 0o775) - subprocess.call(['mv', os.path.expanduser(f'~/.local/bin/blend_{c}/{os.path.basename(i)}.tmp'), - os.path.expanduser(f'~/.local/bin/blend_{c}/{os.path.basename(i)}')]) - _binaries.append((c, os.path.basename(os.path.basename(i)))) - - os.makedirs(os.path.expanduser(f'~/.local/bin/blend_bin'), exist_ok=True) - - for c, i in _binaries: - try: - os.symlink(os.path.expanduser( - f'~/.local/bin/blend_{c}/{i}'), os.path.expanduser(f'~/.local/bin/blend_bin/{i}')) - except FileExistsError: - if subprocess.call(['grep', '-q', f'^# container: {c};{i}$', os.path.expanduser(f'~/.local/bin/blend_bin/{i}')], shell=False): - os.remove(os.path.expanduser(f'~/.local/bin/blend_bin/{i}')) - os.symlink(os.path.expanduser( - f'~/.local/bin/blend_{c}/{i}'), os.path.expanduser(f'~/.local/bin/blend_bin/{i}')) - - for i in remove_binaries: - try: - os.remove(i) - except: - pass - - for b in os.listdir(os.path.expanduser(f'~/.local/bin/blend_bin')): - if [_b for _b in _binaries if _b[1] == b] == []: - os.remove(os.path.join(os.path.expanduser( - f'~/.local/bin/blend_bin'), b)) - - for b_dir in glob.glob(os.path.expanduser(f'~/.local/bin/blend_*')): - if os.path.basename(b_dir) != 'blend_bin' and os.path.basename(b_dir)[6:] not in _list: - subprocess.call(['rm', '-rf', b_dir], shell=False) - - -def create_container_applications(): - _apps = [] - - os.makedirs(os.path.expanduser( - f'~/.local/share/applications'), exist_ok=True) - - for c in _list: - c = c.strip() - for i in con_get_output(c, 'find /usr/share/applications -type f 2>/dev/null; find /usr/local/share/applications -type f 2>/dev/null').split('\n'): - orig_path = i.strip() - i = os.path.basename(orig_path) - i_present = (os.path.isfile(f'/usr/share/applications/{i}') or os.path.isfile(f'/usr/local/share/applications/{i}') - or os.path.isfile(os.path.expanduser(f'~/.local/share/applications/{i}'))) - if i != '' and not i_present: - with open(os.path.expanduser(f'~/.local/share/applications/blend;{i}'), 'w') as f: - _ = con_get_output( - c, f"sudo sed -i '/^DBusActivatable=/d' {orig_path}") - _ = con_get_output( - c, f"sudo sed -i '/^TryExec=/d' {orig_path}") - contents = con_get_output(c, f'cat {orig_path}') - f.write(contents) - for line in fileinput.input(os.path.expanduser(f'~/.local/share/applications/blend;{i}'), inplace=True): - if line.strip().startswith('Exec='): - line = f'Exec=env BLEND_NO_CHECK= blend enter -cn {c} -- {line[5:]}\n' - elif line.strip().startswith('Icon='): - if '/' in line: - line = line.strip() - _ = con_get_output( - c, f"mkdir -p ~/.local/share/blend/icons/file/\"{c}_{i}\"; cp {line[5:]} ~/.local/share/blend/icons/file/\"{c}_{i}\"") - line = f'Icon={os.path.expanduser("~/.local/share/blend/icons/file/" + c + "_" + i + "/" + os.path.basename(line[5:]))}\n' - else: - line = line.strip() - icons = con_get_output(c, f'''find /usr/share/icons /usr/share/pixmaps /var/lib/flatpak/exports/share/icons \\ - -type f -iname "*{line[5:]}*" 2> /dev/null | sort''').split('\r\n') - _ = con_get_output( - c, f"mkdir -p ~/.local/share/blend/icons/\"{c}_{i}\"; cp {icons[0]} ~/.local/share/blend/icons/\"{c}_{i}\"") - line = f'Icon={os.path.expanduser("~/.local/share/blend/icons/" + c + "_" + i + "/" + os.path.basename(icons[0]))}\n' - sys.stdout.write(line) - os.chmod(os.path.expanduser( - f'~/.local/share/applications/blend;{i}'), 0o775) - _apps.append((c, i)) - del _ - - for a in os.listdir(os.path.expanduser(f'~/.local/share/applications')): - if a.startswith('blend;'): - a = a.removeprefix('blend;') - if [_a for _a in _apps if _a[1] == a] == []: - os.remove(os.path.expanduser( - f'~/.local/share/applications/blend;{a}')) - - -def create_container_sessions(type='xsessions'): - session_dir = f'/usr/share/{type}' - - os.makedirs('/usr/share/xsessions', exist_ok=True) - - for session in os.listdir(session_dir): - if session.startswith(os.path.join(session_dir, 'blend-')): - os.remove(os.path.join(session_dir, session)) - - for c in _list: - c = c.strip() - for i in con_get_output(c, f'find {session_dir} -type f 2>/dev/null').split('\n'): - orig_path = i.strip() - i = os.path.basename(orig_path) - if i != '': - with open(os.path.expanduser(f'{session_dir}/blend-{c};{i}'), 'w') as f: - contents = con_get_output(c, f'cat {orig_path}') - f.write(contents) - for line in fileinput.input(os.path.expanduser(f'/{session_dir}/blend-{c};{i}'), inplace=True): - if line.strip().startswith('Name'): - name = line.split('=')[1] - line = f'Name=Container {c}: {name}' - elif line.strip().startswith('Exec='): - line = f'Exec=blend enter -cn {c} -- {line[5:]}' - elif line.strip().startswith('TryExec='): - continue - - sys.stdout.write(line) - os.chmod(os.path.expanduser( - f'{session_dir}/blend-{c};{i}'), 0o775) - - -def con_get_output(name, cmd): - try: - return subprocess.run(['sudo', '-u', user, 'podman', 'exec', '--user', getpass.getuser(), '-it', name, 'bash', '-c', cmd], - stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout=5).stdout.decode('UTF-8').strip() - except subprocess.TimeoutExpired: - return '' - - -user = getpass.getuser() - -try: - user = sys.argv[2] -except: - pass +subprocess.call(['mkdir', '-p', os.path.expanduser('~/.local/bin/blend_bin/')]) +subprocess.call(['touch', os.path.expanduser('~/.local/bin/blend_bin/.associations')], shell=False) for c in get_containers(): c = c.strip() subprocess.call(['podman', 'start', c]) - -while True: - _list = get_containers() - _exceptions = list_use_container_bin() - - create_container_binaries() - create_container_applications() - time.sleep(1) diff --git a/init-blend b/init-blend index c088645..4af0ea6 100755 --- a/init-blend +++ b/init-blend @@ -109,18 +109,18 @@ if command -v apt-get &>/dev/null; then apt-get update &>/dev/null DEBIAN_FRONTEND=noninteractive apt-get -y install bash bc curl less wget apt-utils apt-transport-https dialog \ diffutils findutils gnupg2 sudo time util-linux libnss-myhostname \ - libvte-2.9[0-9]-common libvte-common lsof ncurses-base passwd \ + libvte-2.9[0-9]-common libvte-common lsof ncurses-base passwd inotify-tools \ pinentry-curses libegl1-mesa libgl1-mesa-glx libvulkan1 mesa-vulkan-drivers &>/dev/null elif command -v pacman &>/dev/null; then pacman --noconfirm -Syyu &>/dev/null pacman --noconfirm -Sy bash bc curl wget diffutils findutils gnupg sudo time util-linux vte-common lsof ncurses pinentry \ - mesa opengl-driver vulkan-intel vulkan-radeon base-devel git &>/dev/null + mesa opengl-driver vulkan-intel vulkan-radeon base-devel git inotify-tools &>/dev/null elif command -v dnf &>/dev/null; then dnf install -y --allowerasing bash bc curl wget diffutils findutils dnf-plugins-core gnupg2 less lsof passwd pinentry \ procps-ng vte-profile ncurses util-linux sudo time shadow-utils vulkan mesa-vulkan-drivers \ - mesa-dri-drivers &>/dev/null + mesa-dri-drivers inotify-tools &>/dev/null fi @@ -324,6 +324,23 @@ chmod 4755 /usr/bin/sudo fi +if ! command -v inotify-tools &>/dev/null; then + if command -v apt-get &>/dev/null; then + apt-get update &>/dev/null + DEBIAN_FRONTEND=noninteractive apt-get -y install inotify-tools &>/dev/null + + elif command -v pacman &>/dev/null; then + pacman --noconfirm -Syyu &>/dev/null + pacman --noconfirm -Sy inotify-tools &>/dev/null + + elif command -v dnf &>/dev/null; then + dnf install -y --allowerasing inotify-tools &>/dev/null + fi +fi + +source /run/.containerenv +CONTAINER_NAME="$name" + if [[ ! -f '/.init_blend.lock' ]] && command -v pacman &>/dev/null; then cd /; git clone https://aur.archlinux.org/yay.git &>/dev/null; cd yay chown -R "$_uname" . &>/log @@ -333,12 +350,125 @@ if [[ ! -f '/.init_blend.lock' ]] && command -v pacman &>/dev/null; then touch /.init_blend.lock fi -echo +for full_file in /usr/bin/*; do + if [[ -x "$full_file" ]]; then + file="$(basename "${full_file}")" + if [[ ! -f "${HOME}/.local/bin/blend_bin/${file}.${CONTAINER_NAME}" ]]; then + mkdir -p "${HOME}/.local/bin/blend_bin" + echo "#!/bin/bash" > "${HOME}/.local/bin/blend_bin/${file}.${CONTAINER_NAME}" + echo '[ -f /run/.containerenv ] && { if [[ -e "/usr/bin/'"${file}"'" ]]; then /usr/bin/'"${file}"' "$@"; exit $?; else echo "This command can be accessed from the host, or from the container '"'${CONTAINER_NAME}'"'."; exit 127; fi } || :' >> "${HOME}/.local/bin/blend_bin/${file}.${CONTAINER_NAME}" + echo 'BLEND_ALLOW_ROOT= BLEND_NO_CHECK= blend enter -cn '"${CONTAINER_NAME}"' -- '"${file}"' "$@"' >> "${HOME}/.local/bin/blend_bin/${file}.${CONTAINER_NAME}" + chmod 755 "${HOME}/.local/bin/blend_bin/${file}.${CONTAINER_NAME}" + fi + fi +done + +for full_file in /usr/share/applications/*.desktop; do + file="$(basename "${full_file}")" + + mkdir -p "${HOME}/.local/share/applications" + + echo -n > "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" + + while read -r line; do + if [[ $line == Name* ]]; then + echo "${line} (container ${CONTAINER_NAME})" >> "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" + elif [[ $line == Exec* ]]; then + echo "Exec=blend enter -cn ${CONTAINER_NAME} -- ${line:5}" >> "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" + elif [[ $line == Icon* ]]; then + if [[ -e "${line:5}" ]]; then + mkdir -p "${HOME}/.local/share/blend/icons/${CONTAINER_NAME}_${file}"; cp "${line:5}" "${HOME}/.local/share/blend/icons/${CONTAINER_NAME}_${file}" + echo "Icon=${HOME}/.local/share/blend/icons/${CONTAINER_NAME}_${file}/$(basename "${line:5}")" >> "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" + else + ICON_PATH="$(find /usr/share/icons/hicolor -type f -iname "${line:5}.*" -print -quit 2>/dev/null)" + mkdir -p "$(dirname "${ICON_PATH}" | sed 's/\/usr\/share/'"\/home\/${_uname}"'\/.local\/share/g')" + FINAL_ICON_PATH="$(dirname "${ICON_PATH}" | sed 's/\/usr\/share/'"\/home\/${_uname}"'\/.local\/share/g')/$(echo "${file%.*}").$(basename "${ICON_PATH}" | sed 's/^.*\.//')" + cp "${ICON_PATH}" "${FINAL_ICON_PATH}" &>/dev/null + echo "Icon=${FINAL_ICON_PATH}" >> "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" + fi + else + echo "$line" >> "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" + fi + done < "/usr/share/applications/${file}" + + sed -i 's/DBusActivatable=true/DBusActivatable=false/g' "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" + sed -i '/^TryExec/d' "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" + + chmod 755 "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" +done + echo "Completed container setup." +mkdir -p /usr/share/applications /usr/bin +inotifywait -m /usr/share/applications /usr/bin -e create,delete,move 2>/dev/null | + while read dir action file; do + ( if [[ "$dir" == "/usr/bin/" ]]; then + if [[ "$action" == *"CREATE"* ]]; then + if [[ ! -f "${HOME}/.local/bin/blend_bin/${file}.${CONTAINER_NAME}" ]] && [[ -x "/usr/bin/${file}" ]]; then + mkdir -p "${HOME}/.local/bin/blend_bin" + echo "#!/bin/bash" > "${HOME}/.local/bin/blend_bin/${file}.${CONTAINER_NAME}" + echo '[ -f /run/.containerenv ] && { if [[ -e "/usr/bin/'"${file}"'" ]]; then /usr/bin/'"${file}"' "$@"; exit $?; else echo "This command can be accessed from the host, or from the container '"'${CONTAINER_NAME}'"'."; exit 127; fi } || :' >> "${HOME}/.local/bin/blend_bin/${file}.${CONTAINER_NAME}" + echo 'BLEND_ALLOW_ROOT= BLEND_NO_CHECK= blend enter -cn '"${CONTAINER_NAME}"' -- '"${file}"' "$@"' >> "${HOME}/.local/bin/blend_bin/${file}.${CONTAINER_NAME}" + chmod 755 "${HOME}/.local/bin/blend_bin/${file}.${CONTAINER_NAME}" + fi + elif [[ "$action" == *"DELETE"* ]]; then + rm -f "${HOME}/.local/bin/blend_bin/${file}.${CONTAINER_NAME}" + elif [[ "$action" == *"MOVED_FROM"* ]]; then + rm -f "${HOME}/.local/bin/blend_bin/${file}.${CONTAINER_NAME}" + elif [[ "$action" == *"MOVED_TO"* ]]; then + if [[ ! -f "${HOME}/.local/bin/blend_bin/${file}.${CONTAINER_NAME}" ]] && [[ -x "/usr/bin/${file}" ]]; then + mkdir -p "${HOME}/.local/bin/blend_bin" + echo "#!/bin/bash" > "${HOME}/.local/bin/blend_bin/${file}.${CONTAINER_NAME}" + echo '[ -f /run/.containerenv ] && { if [[ -e "/usr/bin/'"${file}"'" ]]; then /usr/bin/'"${file}"' "$@"; exit $?; else echo "This command can be accessed from the host, or from the container '"'${CONTAINER_NAME}'"'."; exit 127; fi } || :' >> "${HOME}/.local/bin/blend_bin/${file}.${CONTAINER_NAME}" + echo 'BLEND_ALLOW_ROOT= BLEND_NO_CHECK= blend enter -cn '"${CONTAINER_NAME}"' -- '"${file}"' "$@"' >> "${HOME}/.local/bin/blend_bin/${file}.${CONTAINER_NAME}" + chmod 755 "${HOME}/.local/bin/blend_bin/${file}.${CONTAINER_NAME}" + fi + fi + fi ) & + ( if [[ "$dir" == "/usr/share/applications/" ]]; then + if [[ "$action" == *"CREATE"* ]] || [[ "$action" == *"MOVED_TO"* ]]; then + if [[ "$file" == *'.desktop' ]]; then + mkdir -p "${HOME}/.local/share/applications" + + echo -n > "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" + + while read -r line; do + if [[ $line == Name* ]]; then + echo "${line} (${CONTAINER_NAME})" >> "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" + elif [[ $line == Exec* ]]; then + echo "Exec=blend enter -cn ${CONTAINER_NAME} -- ${line:5}" >> "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" + elif [[ $line == Icon* ]]; then + if [[ -e "${line:5}" ]]; then + mkdir -p "${HOME}/.local/share/blend/icons/${CONTAINER_NAME}_${file}"; cp "${line:5}" "${HOME}/.local/share/blend/icons/${CONTAINER_NAME}_${file}" + echo "Icon=${HOME}/.local/share/blend/icons/${CONTAINER_NAME}_${file}/$(basename "${line:5}")" >> "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" + else + ICON_PATH="$(find /usr/share/icons/hicolor -type f -iname "${line:5}.*" -print -quit 2>/dev/null)" + mkdir -p "$(dirname "${ICON_PATH}" | sed 's/\/usr\/share/'"\/home\/${_uname}"'\/.local\/share/g')" + FINAL_ICON_PATH="$(dirname "${ICON_PATH}" | sed 's/\/usr\/share/'"\/home\/${_uname}"'\/.local\/share/g')/$(echo "${file%.*}").$(basename "${ICON_PATH}" | sed 's/^.*\.//')" + cp "${ICON_PATH}" "${FINAL_ICON_PATH}" + echo "Icon=${FINAL_ICON_PATH}" >> "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" + fi + else + echo "$line" >> "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" + fi + done < "/usr/share/applications/${file}" + + sed -i 's/DBusActivatable=true/DBusActivatable=false/g' "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" + sed -i '/^TryExec/d' "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" + + chmod 755 "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" + fi + elif [[ "$action" == *"DELETE"* ]]; then + rm -f "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" + elif [[ "$action" == *"MOVED_FROM"* ]]; then + rm -f "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" + fi + fi ) & + done & + while true; do for i in /etc/hosts /etc/localtime /etc/resolv.conf; do cp "/run/host/${i}" / &>/dev/null || : done sleep 5 -done \ No newline at end of file +done From 82a068799f0dd13e54164b245c339bfe8c8d8135 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Wed, 3 May 2023 00:58:07 +0530 Subject: [PATCH 057/121] Fix multi-window Android apps, move container startup to blendos-first-setup --- blend-files | 17 ++++++----------- blend-settings/src/internal/js/android.js | 7 +++---- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/blend-files b/blend-files index 57b9244..cf95743 100755 --- a/blend-files +++ b/blend-files @@ -3,17 +3,12 @@ import os import subprocess -def get_containers(): - return subprocess.run(['podman', 'ps', '-a', '--no-trunc', '--sort=created', '--format', - '{{.Names}}'], stdout=subprocess.PIPE).stdout.decode('utf-8').strip().split('\n') - if os.path.isdir(os.path.expanduser('~/.local/bin/blend_bin')) and not os.path.isfile(os.path.expanduser('~/.local/bin/blend_bin/.associations')): - subprocess.call(['rm', '-rf', os.path.expanduser('~/.local/bin/blend_bin')], shell=False) - subprocess.call(['bash', '-c', 'rm -f "${HOME}/.local/share/applications/blend;"*'], shell=False) + subprocess.call( + ['rm', '-rf', os.path.expanduser('~/.local/bin/blend_bin')], shell=False) + subprocess.call( + ['bash', '-c', 'rm -f "${HOME}/.local/share/applications/blend;"*'], shell=False) subprocess.call(['mkdir', '-p', os.path.expanduser('~/.local/bin/blend_bin/')]) -subprocess.call(['touch', os.path.expanduser('~/.local/bin/blend_bin/.associations')], shell=False) - -for c in get_containers(): - c = c.strip() - subprocess.call(['podman', 'start', c]) +subprocess.call(['touch', os.path.expanduser( + '~/.local/bin/blend_bin/.associations')], shell=False) diff --git a/blend-settings/src/internal/js/android.js b/blend-settings/src/internal/js/android.js index 9760848..37fd10f 100644 --- a/blend-settings/src/internal/js/android.js +++ b/blend-settings/src/internal/js/android.js @@ -7,15 +7,14 @@ function init_waydroid() { require('child_process').spawnSync('pkexec', ['systemctl', 'enable', '--now', 'waydroid-container']) require('child_process').spawn('sh', ['-c', 'waydroid session start & disown']) setTimeout(() => { - require('child_process').spawnSync('sh', ['-c', 'echo "persist.waydroid.multi_windows=true" | pkexec tee -a /var/lib/waydroid/waydroid_base.prop']) - require('child_process').spawnSync('pkexec', ['waydroid', 'shell', 'pm', 'disable', 'com.android.inputmethod.latin']) + require('child_process').spawnSync('sh', ['-c', 'echo "persist.waydroid.multi_windows=true" | pkexec tee -a /var/lib/waydroid/waydroid.cfg']) if (require('child_process').spawnSync('sh', ['-c', 'LC_ALL=C glxinfo | grep "^OpenGL renderer string: "']).stdout.includes('NVIDIA') || require('child_process').spawnSync('cat', ['/proc/cpuinfo']).stdout.includes('hypervisor')) { require('child_process').spawnSync('sh', ['-c', 'echo "ro.hardware.gralloc=default" | pkexec tee -a /var/lib/waydroid/waydroid.cfg']) require('child_process').spawnSync('sh', ['-c', 'echo "ro.hardware.egl=swiftshader" | pkexec tee -a /var/lib/waydroid/waydroid.cfg']) } require('child_process').spawn('sh', ['-c', 'pkexec waydroid upgrade -o; waydroid session stop; waydroid session start']) - setTimeout(() => { postMessage('success') }, 1000) - }, 8000) + setTimeout(() => { postMessage('success') }, 2000) + }, 4000) ` ) init_worker.onmessage = e => { From 941e43600cb99cec5cf7e44685e017a2c8373d55 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Mon, 8 May 2023 13:10:29 +0530 Subject: [PATCH 058/121] Update to Ubuntu 23.04 --- blend | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/blend b/blend index 6aee307..31f23eb 100755 --- a/blend +++ b/blend @@ -90,7 +90,7 @@ distro_map = { 'arch': 'docker.io/library/archlinux', 'fedora-rawhide': 'docker.io/library/fedora:rawhide', 'ubuntu-22.04': 'docker.io/library/ubuntu:22.04', - 'ubuntu-22.10': 'docker.io/library/ubuntu:22.10' + 'ubuntu-23.04': 'docker.io/library/ubuntu:23.04' } default_distro = 'arch' @@ -428,6 +428,9 @@ def remove_container(): for bin in os.listdir(os.path.expanduser('~/.local/bin/blend_bin')): if bin.endswith(f'.{container}'): os.remove(os.path.join(os.path.expanduser('~/.local/bin/blend_bin'), bin)) + for app in os.listdir(os.path.expanduser('~/.local/share/applications')): + if app.startswith(f'blend;{container};'): + os.remove(os.path.join(os.path.expanduser('~/.local/share/applications'), app)) def start_containers(): From 663e55a753c7b9f64e1a95236d4f6260ab71bdbb Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Mon, 8 May 2023 14:22:22 +0530 Subject: [PATCH 059/121] Update container version in blend-settings --- blend-settings/src/pages/containers.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blend-settings/src/pages/containers.html b/blend-settings/src/pages/containers.html index 59eba24..d53837b 100644 --- a/blend-settings/src/pages/containers.html +++ b/blend-settings/src/pages/containers.html @@ -13,7 +13,7 @@ - +
From 2e0016f941d7b9e91729d952481477b77c5f34c2 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Mon, 8 May 2023 19:58:38 +0530 Subject: [PATCH 060/121] Create an empty desktop file in /usr/share/applications during container creation --- init-blend | 3 +++ 1 file changed, 3 insertions(+) diff --git a/init-blend b/init-blend index 4af0ea6..0412687 100755 --- a/init-blend +++ b/init-blend @@ -363,6 +363,9 @@ for full_file in /usr/bin/*; do fi done +mkdir -p /usr/share/applications +touch "/usr/share/applications/empty_file.desktop" + for full_file in /usr/share/applications/*.desktop; do file="$(basename "${full_file}")" From fd8d0cb9a8d6c9b1411b99143950c410b906d931 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sun, 14 May 2023 19:04:56 +0530 Subject: [PATCH 061/121] feat: make various improvements to blend-settings --- blend-settings/src/index.html | 2 +- blend-settings/src/internal/js/system.js | 135 ----------------------- blend-settings/src/pages/system.html | 46 +------- init-blend | 6 +- 4 files changed, 7 insertions(+), 182 deletions(-) diff --git a/blend-settings/src/index.html b/blend-settings/src/index.html index 314743d..bef71a3 100644 --- a/blend-settings/src/index.html +++ b/blend-settings/src/index.html @@ -18,7 +18,7 @@ Containers - +
diff --git a/blend-settings/src/internal/js/system.js b/blend-settings/src/internal/js/system.js index e65b9ca..2f53ad6 100644 --- a/blend-settings/src/internal/js/system.js +++ b/blend-settings/src/internal/js/system.js @@ -1,96 +1,3 @@ -function rollback() { - let rollback_worker = new Worker( - `data:text/javascript, - let s = require('child_process').spawnSync('pkexec', ['blend-system', 'rollback']).status - if (s === 0) { - postMessage('success') - } else { - postMessage('failure') - } - ` - ) - rollback_worker.onmessage = e => { - if (e.data == 'success') { - document.getElementById('rollback-btn').outerHTML = - '' - } else { - document.getElementById('rollback-btn').outerHTML = - '' - setTimeout(() => document.getElementById('rollback-btn').outerHTML = - '', 2000) - } - } -} - -function undo_rollback() { - let undo_rollback_worker = new Worker( - `data:text/javascript, - let s = require('child_process').spawnSync('pkexec', ['rm', '-f', '/blend/states/.load_prev_state']).status - if (s === 0) { - postMessage('success') - } else { - postMessage('failure') - } - ` - ) - undo_rollback_worker.onmessage = e => { - if (e.data == 'success') { - document.getElementById('rollback-btn').outerHTML = - '' - } else { - document.getElementById('rollback-btn').outerHTML = - '' - setTimeout(() => document.getElementById('rollback-btn').outerHTML = - '', 2000) - } - } -} - -function save_state() { - $("#settings-tabs").find("*").prop('disabled', true) - - let save_state_worker = new Worker( - `data:text/javascript, - let s = require('child_process').spawnSync('pkexec', ['blend-system', 'save-state']).status - if (s === 0) { - postMessage('success') - } else { - postMessage('failure') - } - ` - ) - save_state_worker.onmessage = e => { - if (e.data == 'success') { - document.getElementById('save-state-btn').outerHTML = - '' - $("#settings-tabs").find("*").prop('disabled', false) - setTimeout(() => document.getElementById('save-state-btn').outerHTML = - '', 2000) - } else { - document.getElementById('save-state-btn').outerHTML = - '' - $("#settings-tabs").find("*").prop('disabled', false) - setTimeout(() => document.getElementById('save-state-btn').outerHTML = - '', 2000) - } - } -} - -function check_rollback() { - if (require('fs').existsSync('/blend/states/.load_prev_state')) { - document.getElementById('rollback-btn').outerHTML = - '' - } else { - document.getElementById('rollback-btn').outerHTML = - '' - } -} - -function check_state_creation() { - if (require('fs').existsSync('/blend/states/.disable_states')) { - document.getElementById('automatic-state-toggle').setAttribute('checked', '') - } -} function check_app_grouping() { if (require('fs').existsSync(`${require('os').homedir()}/.config/categorize_apps_gnome_disable`)) { @@ -99,48 +6,6 @@ function check_app_grouping() { } check_app_grouping() -check_state_creation() -check_rollback() - -$('#automatic-state-toggle').on('change', () => { - if (!document.getElementById('automatic-state-toggle').checked) { - let enable_autostate_worker = new Worker( - `data:text/javascript, - let s = require('child_process').spawnSync('pkexec', ['rm', '-f', '/blend/states/.disable_states']).status - if (s === 0) { - postMessage('success') - } else { - postMessage('failure') - } - ` - ) - enable_autostate_worker.onmessage = e => { - if (e.data == 'success') { - document.getElementById('automatic-state-toggle').checked = false - } else { - document.getElementById('automatic-state-toggle').checked = true - } - } - } else { - let disable_autostate_worker = new Worker( - `data:text/javascript, - let s = require('child_process').spawnSync('pkexec', ['blend-system', 'toggle-states']).status - if (s === 0) { - postMessage('success') - } else { - postMessage('failure') - } - ` - ) - disable_autostate_worker.onmessage = e => { - if (e.data == 'success') { - document.getElementById('automatic-state-toggle').checked = true - } else { - document.getElementById('automatic-state-toggle').checked = false - } - } - } -}); $('#app-grouping-toggle').on('change', () => { if (!document.getElementById('app-grouping-toggle').checked) { diff --git a/blend-settings/src/pages/system.html b/blend-settings/src/pages/system.html index 9835988..a7d0525 100644 --- a/blend-settings/src/pages/system.html +++ b/blend-settings/src/pages/system.html @@ -1,50 +1,8 @@
System Settings -

-
-
-
-
- Disable automatic state creation -

blendOS creates copies of apps and config every 12 hours (and keeps the - previous one).

-
-
-
- -
-
-
-
-
-
-
- Save current state -

Create a copy of the current system state, including apps and config.

-
-
-
- -
-
-
-
-
-
-
- Rollback -

Rollback to the most recent state on the next boot. (note: this is irreversible) -

-
-
-
- -
-
-
-
+
+
diff --git a/init-blend b/init-blend index 0412687..1914527 100755 --- a/init-blend +++ b/init-blend @@ -105,6 +105,8 @@ if [[ ! -f '/.init_blend.lock' ]]; then ### +echo 'nameserver 1.1.1.1' > /etc/resolv.conf + if command -v apt-get &>/dev/null; then apt-get update &>/dev/null DEBIAN_FRONTEND=noninteractive apt-get -y install bash bc curl less wget apt-utils apt-transport-https dialog \ @@ -383,7 +385,7 @@ for full_file in /usr/share/applications/*.desktop; do mkdir -p "${HOME}/.local/share/blend/icons/${CONTAINER_NAME}_${file}"; cp "${line:5}" "${HOME}/.local/share/blend/icons/${CONTAINER_NAME}_${file}" echo "Icon=${HOME}/.local/share/blend/icons/${CONTAINER_NAME}_${file}/$(basename "${line:5}")" >> "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" else - ICON_PATH="$(find /usr/share/icons/hicolor -type f -iname "${line:5}.*" -print -quit 2>/dev/null)" + ICON_PATH="$(find /usr/share/icons/hicolor -type f -iname "*${line:5}*" -print -quit 2>/dev/null)" mkdir -p "$(dirname "${ICON_PATH}" | sed 's/\/usr\/share/'"\/home\/${_uname}"'\/.local\/share/g')" FINAL_ICON_PATH="$(dirname "${ICON_PATH}" | sed 's/\/usr\/share/'"\/home\/${_uname}"'\/.local\/share/g')/$(echo "${file%.*}").$(basename "${ICON_PATH}" | sed 's/^.*\.//')" cp "${ICON_PATH}" "${FINAL_ICON_PATH}" &>/dev/null @@ -445,7 +447,7 @@ inotifywait -m /usr/share/applications /usr/bin -e create,delete,move 2>/dev/nul mkdir -p "${HOME}/.local/share/blend/icons/${CONTAINER_NAME}_${file}"; cp "${line:5}" "${HOME}/.local/share/blend/icons/${CONTAINER_NAME}_${file}" echo "Icon=${HOME}/.local/share/blend/icons/${CONTAINER_NAME}_${file}/$(basename "${line:5}")" >> "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" else - ICON_PATH="$(find /usr/share/icons/hicolor -type f -iname "${line:5}.*" -print -quit 2>/dev/null)" + ICON_PATH="$(find /usr/share/icons/hicolor -type f -iname "*${line:5}*" -print -quit 2>/dev/null)" mkdir -p "$(dirname "${ICON_PATH}" | sed 's/\/usr\/share/'"\/home\/${_uname}"'\/.local\/share/g')" FINAL_ICON_PATH="$(dirname "${ICON_PATH}" | sed 's/\/usr\/share/'"\/home\/${_uname}"'\/.local\/share/g')/$(echo "${file%.*}").$(basename "${ICON_PATH}" | sed 's/^.*\.//')" cp "${ICON_PATH}" "${FINAL_ICON_PATH}" From 237708a95e26b253b15408d9f515eaebb0233b76 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sun, 14 May 2023 19:34:02 +0530 Subject: [PATCH 062/121] chore: update pkgver --- PKGBUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PKGBUILD b/PKGBUILD index 3b87f48..89fb7c1 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -2,7 +2,7 @@ pkgbase=blend-git pkgname=('blend-git' 'blend-settings-git') -pkgver=r42.158cc38 +pkgver=r50.2e0016f pkgrel=1 _electronversion=22 pkgdesc="A package manager for blendOS" From 3e8089d2dd554bd571a7f6788caaf2794a96f804 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Mon, 15 May 2023 16:24:05 +0530 Subject: [PATCH 063/121] feat: set set-fix-to-user-rotation to enabled --- blend-settings/src/internal/js/android.js | 1 + 1 file changed, 1 insertion(+) diff --git a/blend-settings/src/internal/js/android.js b/blend-settings/src/internal/js/android.js index 37fd10f..2cd26ed 100644 --- a/blend-settings/src/internal/js/android.js +++ b/blend-settings/src/internal/js/android.js @@ -7,6 +7,7 @@ function init_waydroid() { require('child_process').spawnSync('pkexec', ['systemctl', 'enable', '--now', 'waydroid-container']) require('child_process').spawn('sh', ['-c', 'waydroid session start & disown']) setTimeout(() => { + require('child_process').spawnSync('sh', ['-c', 'pkexec waydroid shell wm set-fix-to-user-rotation enabled']) require('child_process').spawnSync('sh', ['-c', 'echo "persist.waydroid.multi_windows=true" | pkexec tee -a /var/lib/waydroid/waydroid.cfg']) if (require('child_process').spawnSync('sh', ['-c', 'LC_ALL=C glxinfo | grep "^OpenGL renderer string: "']).stdout.includes('NVIDIA') || require('child_process').spawnSync('cat', ['/proc/cpuinfo']).stdout.includes('hypervisor')) { require('child_process').spawnSync('sh', ['-c', 'echo "ro.hardware.gralloc=default" | pkexec tee -a /var/lib/waydroid/waydroid.cfg']) From 065ff8ec2a7e5a240d9a2f43d418758be73da42c Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Tue, 16 May 2023 23:12:52 +0530 Subject: [PATCH 064/121] feat: add support for debian, kali and neurodebian --- blend | 4 ++++ blend-settings/src/pages/containers.html | 3 +++ init-blend | 6 +++--- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/blend b/blend index 31f23eb..039f96e 100755 --- a/blend +++ b/blend @@ -88,6 +88,10 @@ def error(err): distro_map = { 'arch': 'docker.io/library/archlinux', + 'debian': 'docker.io/library/debian:latest', + 'neurodebian': 'docker.io/library/neurodebian:nd120', + 'kali-linux-rolling': 'docker.io/kalilinux/kali-rolling', + 'kali-linux-rolling': 'docker.io/kalilinux/kali-rolling', 'fedora-rawhide': 'docker.io/library/fedora:rawhide', 'ubuntu-22.04': 'docker.io/library/ubuntu:22.04', 'ubuntu-23.04': 'docker.io/library/ubuntu:23.04' diff --git a/blend-settings/src/pages/containers.html b/blend-settings/src/pages/containers.html index d53837b..1a628c2 100644 --- a/blend-settings/src/pages/containers.html +++ b/blend-settings/src/pages/containers.html @@ -11,6 +11,9 @@
+ - - + + + From 0eb49c1412788a3d808d4d0681a264ac5258c4aa Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Wed, 17 May 2023 10:32:22 +0530 Subject: [PATCH 066/121] feat: add support for rocky linux and almalinux --- blend | 2 ++ blend-settings/src/pages/containers.html | 2 ++ init-blend | 2 ++ 3 files changed, 6 insertions(+) diff --git a/blend b/blend index c6e8ea0..074b5a6 100755 --- a/blend +++ b/blend @@ -88,12 +88,14 @@ def error(err): distro_map = { 'arch': 'docker.io/library/archlinux', + 'almalinux-9': 'quay.io/almalinux/almalinux:9', 'crystal-linux': 'registry.getcryst.al/crystal/misc/docker:latest', 'debian': 'docker.io/library/debian:latest', 'fedora-rawhide': 'docker.io/library/fedora:rawhide', 'kali-linux-rolling': 'docker.io/kalilinux/kali-rolling', 'manjaro-linux': 'docker.io/manjarolinux/base:latest', 'neurodebian-bookworm': 'docker.io/library/neurodebian:nd120', + 'rocky-linux-9': 'docker.io/rockylinux/rockylinux:9', 'ubuntu-22.04': 'docker.io/library/ubuntu:22.04', 'ubuntu-23.04': 'docker.io/library/ubuntu:23.04' } diff --git a/blend-settings/src/pages/containers.html b/blend-settings/src/pages/containers.html index fbe3bc9..69775ca 100644 --- a/blend-settings/src/pages/containers.html +++ b/blend-settings/src/pages/containers.html @@ -11,12 +11,14 @@
diff --git a/init-blend b/init-blend index c5dbd81..1173017 100755 --- a/init-blend +++ b/init-blend @@ -120,6 +120,8 @@ elif command -v pacman &>/dev/null; then mesa opengl-driver vulkan-intel vulkan-radeon base-devel git inotify-tools &>/dev/null elif command -v dnf &>/dev/null; then + dnf config-manager --set-enabled crb &>/dev/null + dnf install -y epel-release &>/dev/null dnf install -y --allowerasing bash bc curl wget diffutils findutils dnf-plugins-core gnupg2 less lsof passwd pinentry \ procps-ng vte-profile ncurses util-linux sudo time shadow-utils vulkan mesa-vulkan-drivers \ mesa-dri-drivers inotify-tools &>/dev/null From 66494a7f28a054f1e531ef98b38c4b92de1f5722 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Thu, 18 May 2023 21:26:34 +0530 Subject: [PATCH 067/121] feat: fix container issues --- blend | 5 ++--- blend-settings/src/pages/containers.html | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/blend b/blend index 074b5a6..d4410b5 100755 --- a/blend +++ b/blend @@ -92,10 +92,9 @@ distro_map = { 'crystal-linux': 'registry.getcryst.al/crystal/misc/docker:latest', 'debian': 'docker.io/library/debian:latest', 'fedora-rawhide': 'docker.io/library/fedora:rawhide', - 'kali-linux-rolling': 'docker.io/kalilinux/kali-rolling', - 'manjaro-linux': 'docker.io/manjarolinux/base:latest', + 'kali-linux': 'docker.io/kalilinux/kali-rolling', 'neurodebian-bookworm': 'docker.io/library/neurodebian:nd120', - 'rocky-linux-9': 'docker.io/rockylinux/rockylinux:9', + 'rocky-linux': 'docker.io/rockylinux/rockylinux:9', 'ubuntu-22.04': 'docker.io/library/ubuntu:22.04', 'ubuntu-23.04': 'docker.io/library/ubuntu:23.04' } diff --git a/blend-settings/src/pages/containers.html b/blend-settings/src/pages/containers.html index 69775ca..bd87549 100644 --- a/blend-settings/src/pages/containers.html +++ b/blend-settings/src/pages/containers.html @@ -15,10 +15,9 @@ - - + - + From a60dd9ab7d07b6839ccd2c3bec082fa58dd80e2b Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sat, 20 May 2023 14:50:11 +0530 Subject: [PATCH 068/121] feat: fix broken Ubuntu container --- blend-settings/src/internal/js/android.js | 2 +- init-blend | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/blend-settings/src/internal/js/android.js b/blend-settings/src/internal/js/android.js index 2cd26ed..40391fc 100644 --- a/blend-settings/src/internal/js/android.js +++ b/blend-settings/src/internal/js/android.js @@ -15,7 +15,7 @@ function init_waydroid() { } require('child_process').spawn('sh', ['-c', 'pkexec waydroid upgrade -o; waydroid session stop; waydroid session start']) setTimeout(() => { postMessage('success') }, 2000) - }, 4000) + }, 8000) ` ) init_worker.onmessage = e => { diff --git a/init-blend b/init-blend index 1173017..3d3f24b 100755 --- a/init-blend +++ b/init-blend @@ -105,8 +105,6 @@ if [[ ! -f '/.init_blend.lock' ]]; then ### -echo 'nameserver 1.1.1.1' > /etc/resolv.conf - if command -v apt-get &>/dev/null; then apt-get update &>/dev/null DEBIAN_FRONTEND=noninteractive apt-get -y install bash bc curl less wget apt-utils apt-transport-https dialog \ @@ -320,6 +318,7 @@ if ! grep -q "^${_uname}:" /etc/group; then printf "%s:x:%s:" "$_uname" "$_cgid" >> /etc/group fi fi +userdel -r ubuntu &>/dev/null useradd --uid "$_cuid" --gid "$_cgid" --shell "/bin/bash" --no-create-home --home "$_uhome" "$_uname" &>/dev/null chown root /etc/sudo.conf From 635d08594383d4530b3a4d36dd0949d68b385f9c Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sun, 21 May 2023 12:44:01 +0530 Subject: [PATCH 069/121] feat: fix all Ubuntu container issues --- init-blend | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/init-blend b/init-blend index 3d3f24b..f0af208 100755 --- a/init-blend +++ b/init-blend @@ -318,7 +318,15 @@ if ! grep -q "^${_uname}:" /etc/group; then printf "%s:x:%s:" "$_uname" "$_cgid" >> /etc/group fi fi -userdel -r ubuntu &>/dev/null +if [[ $_cuid -eq 1000 ]] && grep -q ubuntu /etc/passwd; then + userdel -r ubuntu &>/dev/null + groupdel ubuntu &> /dev/null + grep -v ubuntu /etc/passwd > /etc/passwd.tmp + grep -v ubuntu /etc/group > /etc/group.tmp + mv /etc/passwd.tmp /etc/passwd + mv /etc/group.tmp /etc/group + echo -e '\n'"$_uname"':x:1000\n' >> /etc/group +fi useradd --uid "$_cuid" --gid "$_cgid" --shell "/bin/bash" --no-create-home --home "$_uhome" "$_uname" &>/dev/null chown root /etc/sudo.conf From 9734d9daafd4590fbcfa4159a2d3a2dbf203e3c5 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sun, 21 May 2023 17:39:00 +0530 Subject: [PATCH 070/121] feat: get user creation working for Ubuntu 23.04 containers --- init-blend | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/init-blend b/init-blend index f0af208..7cefe52 100755 --- a/init-blend +++ b/init-blend @@ -325,7 +325,11 @@ if [[ $_cuid -eq 1000 ]] && grep -q ubuntu /etc/passwd; then grep -v ubuntu /etc/group > /etc/group.tmp mv /etc/passwd.tmp /etc/passwd mv /etc/group.tmp /etc/group - echo -e '\n'"$_uname"':x:1000\n' >> /etc/group +fi +if ! grep -q "^${_uname}:" /etc/group; then + if ! groupadd --force --gid "$_cgid" "$_uname"; then + printf "%s:x:%s:" "$_uname" "$_cgid" >> /etc/group + fi fi useradd --uid "$_cuid" --gid "$_cgid" --shell "/bin/bash" --no-create-home --home "$_uhome" "$_uname" &>/dev/null From 4f7c481ce94ba40a47d09e08a0726591ba388c4a Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Wed, 24 May 2023 15:44:49 +0530 Subject: [PATCH 071/121] feat: make packaging compatible with build tools --- init-blend | 5 ----- 1 file changed, 5 deletions(-) diff --git a/init-blend b/init-blend index 7cefe52..8a4f607 100755 --- a/init-blend +++ b/init-blend @@ -313,11 +313,6 @@ fi if ! grep -q "\"${_uname}\" ALL = (root) NOPASSWD:ALL" /etc/sudoers.d/sudoers &>/dev/null; then printf "\"%s\" ALL = (root) NOPASSWD:ALL\n" "$_uname" >> /etc/sudoers.d/sudoers fi -if ! grep -q "^${_uname}:" /etc/group; then - if ! groupadd --force --gid "$_cgid" "$_uname"; then - printf "%s:x:%s:" "$_uname" "$_cgid" >> /etc/group - fi -fi if [[ $_cuid -eq 1000 ]] && grep -q ubuntu /etc/passwd; then userdel -r ubuntu &>/dev/null groupdel ubuntu &> /dev/null From 1b8f8703c6aff38fc08a9e32ca3c4a92e5379b08 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Wed, 24 May 2023 15:48:14 +0530 Subject: [PATCH 072/121] feat: make packaging compatible with build tools --- PKGBUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PKGBUILD b/PKGBUILD index 89fb7c1..5851b06 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -10,7 +10,7 @@ arch=('x86_64' 'i686') url="https://github.com/blend-os/blend" license=('GPL3') makedepends=("electron${_electronversion}" 'git' 'npm' 'base-devel') -source=('git+https://github.com/blend-os/blend.git' +source=('git+file://[BASE_ASSEMBLE_PATH]/projects/blend' 'blend-settings.desktop' 'blend-settings' 'blend.sh') From e1374c1254c3312475a5da03c4fdf88d4d0f3d56 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sun, 28 May 2023 07:50:58 +0530 Subject: [PATCH 073/121] feat (blend-settings): add support for copying text, switch to JetBrains Mono --- blend-settings/main.js | 2 +- blend-settings/src/pages/terminal.html | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/blend-settings/main.js b/blend-settings/main.js index 7c13b8a..5da0ebd 100644 --- a/blend-settings/main.js +++ b/blend-settings/main.js @@ -44,7 +44,7 @@ function createTerminalWindow() { terminalWindow.loadFile('src/pages/terminal.html') - terminalWindow.setMenu(null) + // terminalWindow.setMenu(null) } function loadTerminalWindow(title, cmd) { diff --git a/blend-settings/src/pages/terminal.html b/blend-settings/src/pages/terminal.html index 090d0dc..628c076 100644 --- a/blend-settings/src/pages/terminal.html +++ b/blend-settings/src/pages/terminal.html @@ -69,13 +69,25 @@ experimentalCharAtlas: 'dynamic', theme: { background: '#242430' - } + }, + fontFamily: 'JetBrains Mono' }); once = true term.loadAddon(fit) + term.attachCustomKeyEventHandler((arg) => { + if (arg.ctrlKey && arg.shiftKey && arg.code === "KeyC" && arg.type === "keydown") { + const selection = term.getSelection(); + if (selection) { + navigator.clipboard.writeText(selection); + return false; + } + } + return true; + }); + var term_e = document.getElementById('term'); term.open(term_e); fit.fit() From 82b5e7600d3568bf13e533e159f6a541a130fe30 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sun, 4 Jun 2023 20:54:34 +0530 Subject: [PATCH 074/121] chore: switch back to Fedora 38, due to rawhide breaking regularly --- blend | 2 +- blend-settings/src/pages/containers.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/blend b/blend index d4410b5..ad8ab7d 100755 --- a/blend +++ b/blend @@ -91,7 +91,7 @@ distro_map = { 'almalinux-9': 'quay.io/almalinux/almalinux:9', 'crystal-linux': 'registry.getcryst.al/crystal/misc/docker:latest', 'debian': 'docker.io/library/debian:latest', - 'fedora-rawhide': 'docker.io/library/fedora:rawhide', + 'fedora-38': 'docker.io/library/fedora:38', 'kali-linux': 'docker.io/kalilinux/kali-rolling', 'neurodebian-bookworm': 'docker.io/library/neurodebian:nd120', 'rocky-linux': 'docker.io/rockylinux/rockylinux:9', diff --git a/blend-settings/src/pages/containers.html b/blend-settings/src/pages/containers.html index bd87549..091d645 100644 --- a/blend-settings/src/pages/containers.html +++ b/blend-settings/src/pages/containers.html @@ -14,7 +14,7 @@ - + From 8bfc04a97ca8a62ffc62268ebce72ff1ceed969f Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Tue, 13 Jun 2023 21:01:47 +1000 Subject: [PATCH 075/121] chore (blend-settings): use blend to remove containers, instead of podman --- blend-settings/src/internal/js/containers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blend-settings/src/internal/js/containers.js b/blend-settings/src/internal/js/containers.js index 43f6c1f..c6bf249 100644 --- a/blend-settings/src/internal/js/containers.js +++ b/blend-settings/src/internal/js/containers.js @@ -33,7 +33,7 @@ async function remove_container(name) { let rm_worker = new Worker( `data:text/javascript, require('child_process').spawnSync('podman', ['stop', '-t', '0', '${name}'], { encoding: 'utf8' }) - require('child_process').spawnSync('podman', ['rm', '-f', '${name}'], { encoding: 'utf8' }) + require('child_process').spawnSync('blend', ['remove-container', '${name}'], { encoding: 'utf8' }) postMessage('') ` ) From 5ecc7898d68961308aad7bcf291dce08b9927db1 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Wed, 14 Jun 2023 18:01:29 +1000 Subject: [PATCH 076/121] chore (blend-settings): improve look and feel --- blend-settings/src/internal/css/common.css | 6 +++++- blend-settings/src/pages/containers.html | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/blend-settings/src/internal/css/common.css b/blend-settings/src/internal/css/common.css index 47188f8..91ae09c 100644 --- a/blend-settings/src/internal/css/common.css +++ b/blend-settings/src/internal/css/common.css @@ -1,6 +1,6 @@ body { margin: 0 !important; - background-color: rgb(36, 36, 48); + background-color: rgb(24, 24, 32); color: rgba(240, 240, 255, 1); overflow: hidden; } @@ -80,6 +80,10 @@ select option { top: 0.67em; } +.form-control:focus { + box-shadow: 0 0 0 0.25rem rgb(37, 124, 253) !important; +} + .list-group-item { background-color: rgb(29, 29, 37); color: #fff; diff --git a/blend-settings/src/pages/containers.html b/blend-settings/src/pages/containers.html index 091d645..d7bf67b 100644 --- a/blend-settings/src/pages/containers.html +++ b/blend-settings/src/pages/containers.html @@ -23,7 +23,7 @@
-
@@ -56,7 +56,7 @@
-
From e1eda06edcc85c293ebb6049cecee37d76c0521d Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Thu, 15 Jun 2023 10:05:07 +1000 Subject: [PATCH 077/121] chore: create ~/.local/share/applications --- blend-files | 1 + 1 file changed, 1 insertion(+) diff --git a/blend-files b/blend-files index cf95743..d7b087b 100755 --- a/blend-files +++ b/blend-files @@ -9,6 +9,7 @@ if os.path.isdir(os.path.expanduser('~/.local/bin/blend_bin')) and not os.path.i subprocess.call( ['bash', '-c', 'rm -f "${HOME}/.local/share/applications/blend;"*'], shell=False) +subprocess.call(['mkdir', '-p', os.path.expanduser('~/.local/share/applications/')]) subprocess.call(['mkdir', '-p', os.path.expanduser('~/.local/bin/blend_bin/')]) subprocess.call(['touch', os.path.expanduser( '~/.local/bin/blend_bin/.associations')], shell=False) From c7e6bff6a29f5deb6c130383fd4fd1c7f33d9b19 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Wed, 21 Jun 2023 10:18:33 +1000 Subject: [PATCH 078/121] chore (blend-settings): reduce number of times entering your password is required during Android init --- blend-settings/src/internal/js/android.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/blend-settings/src/internal/js/android.js b/blend-settings/src/internal/js/android.js index 40391fc..8421876 100644 --- a/blend-settings/src/internal/js/android.js +++ b/blend-settings/src/internal/js/android.js @@ -7,11 +7,9 @@ function init_waydroid() { require('child_process').spawnSync('pkexec', ['systemctl', 'enable', '--now', 'waydroid-container']) require('child_process').spawn('sh', ['-c', 'waydroid session start & disown']) setTimeout(() => { - require('child_process').spawnSync('sh', ['-c', 'pkexec waydroid shell wm set-fix-to-user-rotation enabled']) - require('child_process').spawnSync('sh', ['-c', 'echo "persist.waydroid.multi_windows=true" | pkexec tee -a /var/lib/waydroid/waydroid.cfg']) + require('child_process').spawnSync('sh', ['-c', 'pkexec bash -c "waydroid shell wm set-fix-to-user-rotation enabled; echo persist.waydroid.multi_windows=true | tee -a /var/lib/waydroid/waydroid.cfg"']) if (require('child_process').spawnSync('sh', ['-c', 'LC_ALL=C glxinfo | grep "^OpenGL renderer string: "']).stdout.includes('NVIDIA') || require('child_process').spawnSync('cat', ['/proc/cpuinfo']).stdout.includes('hypervisor')) { - require('child_process').spawnSync('sh', ['-c', 'echo "ro.hardware.gralloc=default" | pkexec tee -a /var/lib/waydroid/waydroid.cfg']) - require('child_process').spawnSync('sh', ['-c', 'echo "ro.hardware.egl=swiftshader" | pkexec tee -a /var/lib/waydroid/waydroid.cfg']) + require('child_process').spawnSync('sh', ['-c', 'echo -e "ro.hardware.gralloc=default\\nro.hardware.egl=swiftshader" | pkexec tee -a /var/lib/waydroid/waydroid.cfg']) } require('child_process').spawn('sh', ['-c', 'pkexec waydroid upgrade -o; waydroid session stop; waydroid session start']) setTimeout(() => { postMessage('success') }, 2000) From 7a029d159502ec90e25b857d288d6a459c6abecd Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sat, 24 Jun 2023 15:13:36 +1000 Subject: [PATCH 079/121] feat: new package installler --- blend-settings/main.js | 59 ++++++-- blend-settings/package.json | 10 +- blend-settings/src/index.html | 12 +- blend-settings/src/internal/js/android.js | 2 +- blend-settings/src/package-installer.html | 170 ++++++++++++++++++++++ blend-settings/static/APK.svg | 1 + blend-settings/static/DEB.svg | 1 + blend-settings/static/RPM.svg | 1 + 8 files changed, 231 insertions(+), 25 deletions(-) create mode 100644 blend-settings/src/package-installer.html create mode 100644 blend-settings/static/APK.svg create mode 100644 blend-settings/static/DEB.svg create mode 100644 blend-settings/static/RPM.svg diff --git a/blend-settings/main.js b/blend-settings/main.js index 5da0ebd..cb8914a 100644 --- a/blend-settings/main.js +++ b/blend-settings/main.js @@ -2,11 +2,13 @@ const { app, BrowserWindow, ipcMain, dialog } = require('electron') const path = require('path') const pty = require("node-pty"); -var mainWindow, terminalWindow, ptyProcess +var mainWindow, terminalWindow, packageWindow, ptyProcess app.commandLine.appendSwitch('enable-transparent-visuals'); app.disableHardwareAcceleration(); +require('@electron/remote/main').initialize() + function createWindow() { mainWindow = new BrowserWindow({ minWidth: 1000, @@ -30,7 +32,7 @@ function createWindow() { } function createTerminalWindow() { - terminalWindow = new BrowserWindow({ + let t_window_settings = { minWidth: 800, minHeight: 600, webPreferences: { @@ -40,13 +42,43 @@ function createTerminalWindow() { }, autoHideMenuBar: true, show: false - }) + } + + if (process.argv.length > 2) { + if (process.argv[2] == 'package') { + t_window_settings['frame'] = false + } + } + + terminalWindow = new BrowserWindow(t_window_settings) terminalWindow.loadFile('src/pages/terminal.html') // terminalWindow.setMenu(null) } +function createPackageWindow() { + packageWindow = new BrowserWindow({ + minWidth: 450, + minHeight: 450, + width: 450, + height: 450, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + enableRemoteModule: true, + sandbox: false + }, + autoHideMenuBar: true + }) + + packageWindow.loadFile('src/package-installer.html') + + require("@electron/remote/main").enable(packageWindow.webContents) + + // packageWindow.setMenu(null) +} + function loadTerminalWindow(title, cmd) { if (terminalWindow.isDestroyed()) { createTerminalWindow() @@ -98,6 +130,8 @@ function loadTerminalWindow(title, cmd) { ptyProcess.destroy() } }) + } else if (terminalWindow.getTitle().startsWith('Package installation')) { + e.preventDefault() } else { terminalWindow.hide() ptyProcess.destroy() @@ -128,6 +162,8 @@ function loadTerminalWindow(title, cmd) { terminalWindow.hide() if (title.startsWith('Creating container: ')) { mainWindow.webContents.send("container-created") + } else if (title.startsWith('Package installation')) { + packageWindow.webContents.send("installation-complete") } } }) @@ -142,18 +178,21 @@ function loadTerminalWindow(title, cmd) { app.whenReady().then(() => { app.allowRendererProcessReuse = false - setTimeout(() => { - createWindow(); - }, 1000); + if (process.argv.length > 2) { + if (process.argv[2] == 'package') { + createPackageWindow() + } + } else { + setTimeout(() => { + createWindow() + }, 1000); + } + createTerminalWindow() ipcMain.on('create-term', (event, data) => { loadTerminalWindow(data['title'], data['cmd']) }) - - app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0) createWindow() - }) }) diff --git a/blend-settings/package.json b/blend-settings/package.json index 075c4d5..a18cc34 100644 --- a/blend-settings/package.json +++ b/blend-settings/package.json @@ -14,7 +14,7 @@ "dist": "electron-builder" }, "dependencies": { - "@electron/remote": "^2.0.9", + "@electron/remote": "^2.0.10", "@types/jquery": "^3.5.16", "jquery": "^3.6.3", "js-yaml": "^4.1.0", @@ -26,15 +26,17 @@ }, "devDependencies": { "electron": "^23.0.0", - "electron-icon-maker": "^0.0.5", - "electron-builder": "^23.6.0" + "electron-builder": "^23.6.0", + "electron-icon-maker": "^0.0.5" }, "build": { "appId": "org.blend.settings", "productName": "blendOS Settings", "asar": true, "linux": { - "target": ["tar.gz"], + "target": [ + "tar.gz" + ], "category": "System", "icon": "icons/png", "maintainer": "Rudra Saraswat" diff --git a/blend-settings/src/index.html b/blend-settings/src/index.html index bef71a3..d6edc1d 100644 --- a/blend-settings/src/index.html +++ b/blend-settings/src/index.html @@ -4,15 +4,7 @@ Settings - - - - - - - -
-
+ f
@@ -25,7 +17,7 @@ - + + + \ No newline at end of file diff --git a/blend-settings/static/APK.svg b/blend-settings/static/APK.svg new file mode 100644 index 0000000..0b88e35 --- /dev/null +++ b/blend-settings/static/APK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/blend-settings/static/DEB.svg b/blend-settings/static/DEB.svg new file mode 100644 index 0000000..3fd7cb9 --- /dev/null +++ b/blend-settings/static/DEB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/blend-settings/static/RPM.svg b/blend-settings/static/RPM.svg new file mode 100644 index 0000000..0cd203f --- /dev/null +++ b/blend-settings/static/RPM.svg @@ -0,0 +1 @@ + \ No newline at end of file From 811971ce2637a8c11800f1fd8ab01edf2b713f7a Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sat, 24 Jun 2023 22:24:57 +1000 Subject: [PATCH 080/121] chore: set cursor to default for buttons --- blend-settings/src/package-installer.html | 1 + 1 file changed, 1 insertion(+) diff --git a/blend-settings/src/package-installer.html b/blend-settings/src/package-installer.html index 6de7a6b..186d2fa 100644 --- a/blend-settings/src/package-installer.html +++ b/blend-settings/src/package-installer.html @@ -61,6 +61,7 @@ } button:hover { + cursor: default; background-color: #3271be; } From 525fe9a26901476b23319b3d4cd6a865f227ab9b Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sat, 24 Jun 2023 22:40:14 +1000 Subject: [PATCH 081/121] chore: undo accidental changes --- blend-settings/src/index.html | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/blend-settings/src/index.html b/blend-settings/src/index.html index d6edc1d..bef71a3 100644 --- a/blend-settings/src/index.html +++ b/blend-settings/src/index.html @@ -4,7 +4,15 @@ Settings - f + + + + + + + +
+
@@ -17,7 +25,7 @@ - From b9f130610f06134ff7bd11c36fa99b9de2b0e720 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sun, 25 Jun 2023 16:45:19 +1000 Subject: [PATCH 084/121] feat: add blend-package-installer.desktop --- PKGBUILD | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/PKGBUILD b/PKGBUILD index 1fe6fef..f21d87c 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -11,10 +11,12 @@ license=('GPL3') makedepends=("electron" 'git' 'npm' 'base-devel') source=('git+file://[BASE_ASSEMBLE_PATH]/projects/blend' 'blend-settings.desktop' + 'blend-package-installer.desktop' 'blend-settings' 'blend.sh') sha256sums=('SKIP' 'a605d24d2fa7384b45a94105143db216db1ffc0bdfc7f6eec758ef2026e61e54' + '23decd858ab49e860999bba783da78f43adc7b678065057cadfc2eeaefb2e870' '73cb7c39190d36f233b8dfbc3e3e6737d56e61e90881ad95f09e5ae1f9b405a8' 'SKIP') pkgver() { @@ -96,5 +98,8 @@ package_blend-settings-git() { install -Dm644 "${srcdir}/${pkgname%-git}.desktop" -t \ "${pkgdir}"/usr/share/applications/ + install -Dm644 "${srcdir}/blend-package-installer.desktop" -t \ + "${pkgdir}"/usr/share/applications/ + install -Dm755 "${srcdir}/${pkgname%-git}" -t "${pkgdir}"/usr/bin/ } From d36c1db8c69e1fb75a17f0c76d38b8bc5ea3942a Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sun, 25 Jun 2023 21:28:14 +1000 Subject: [PATCH 085/121] chore: remove blend-package-installer.desktop from .gitignore --- .gitignore | 1 + blend-package-installer.desktop | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 blend-package-installer.desktop diff --git a/.gitignore b/.gitignore index c565390..602e85a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ !blend.sh !blend-settings !blend-settings.desktop +!blend-package-installer.desktop !.SRCINFO diff --git a/blend-package-installer.desktop b/blend-package-installer.desktop new file mode 100644 index 0000000..be2702d --- /dev/null +++ b/blend-package-installer.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=Package Installer +Exec=blend-settings package %F +Terminal=false +NoDisplay=true +Type=Application +Icon=blend-settings +StartupWMClass=blend-settings +Comment=Package installer for blendOS. +Categories=System; From 9850a9d8ca8b7126faf8ff39813f50b6675676dd Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Mon, 26 Jun 2023 11:26:03 +1000 Subject: [PATCH 086/121] chore: create an empty file in ~/.local/share/applications --- blend-files | 2 ++ init-blend | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/blend-files b/blend-files index d7b087b..1e04e61 100755 --- a/blend-files +++ b/blend-files @@ -13,3 +13,5 @@ subprocess.call(['mkdir', '-p', os.path.expanduser('~/.local/share/applications/ subprocess.call(['mkdir', '-p', os.path.expanduser('~/.local/bin/blend_bin/')]) subprocess.call(['touch', os.path.expanduser( '~/.local/bin/blend_bin/.associations')], shell=False) +subprocess.call(['touch', os.path.expanduser( + '~/.local/share/applications/.empty')], shell=False) diff --git a/init-blend b/init-blend index 8a4f607..84fd060 100755 --- a/init-blend +++ b/init-blend @@ -379,8 +379,6 @@ touch "/usr/share/applications/empty_file.desktop" for full_file in /usr/share/applications/*.desktop; do file="$(basename "${full_file}")" - mkdir -p "${HOME}/.local/share/applications" - echo -n > "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" while read -r line; do @@ -441,8 +439,6 @@ inotifywait -m /usr/share/applications /usr/bin -e create,delete,move 2>/dev/nul ( if [[ "$dir" == "/usr/share/applications/" ]]; then if [[ "$action" == *"CREATE"* ]] || [[ "$action" == *"MOVED_TO"* ]]; then if [[ "$file" == *'.desktop' ]]; then - mkdir -p "${HOME}/.local/share/applications" - echo -n > "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" while read -r line; do From ef3c7a911d7f7328b1828995b3149eefb1ad4ecd Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Mon, 26 Jun 2023 20:36:48 +1000 Subject: [PATCH 087/121] chore: add dbus to init-blend --- init-blend | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/init-blend b/init-blend index 84fd060..ed00527 100755 --- a/init-blend +++ b/init-blend @@ -110,19 +110,19 @@ if command -v apt-get &>/dev/null; then DEBIAN_FRONTEND=noninteractive apt-get -y install bash bc curl less wget apt-utils apt-transport-https dialog \ diffutils findutils gnupg2 sudo time util-linux libnss-myhostname \ lsof ncurses-base passwd inotify-tools pinentry-curses libegl1-mesa \ - libgl1-mesa-glx libvulkan1 mesa-vulkan-drivers &>/dev/null + libgl1-mesa-glx libvulkan1 mesa-vulkan-drivers dbus &>/dev/null elif command -v pacman &>/dev/null; then pacman --noconfirm -Syyu &>/dev/null pacman --noconfirm -Sy bash bc curl wget diffutils findutils gnupg sudo time util-linux vte-common lsof ncurses pinentry \ - mesa opengl-driver vulkan-intel vulkan-radeon base-devel git inotify-tools &>/dev/null + mesa opengl-driver vulkan-intel vulkan-radeon base-devel git inotify-tools dbus dbus-broker &>/dev/null elif command -v dnf &>/dev/null; then dnf config-manager --set-enabled crb &>/dev/null dnf install -y epel-release &>/dev/null dnf install -y --allowerasing bash bc curl wget diffutils findutils dnf-plugins-core gnupg2 less lsof passwd pinentry \ procps-ng vte-profile ncurses util-linux sudo time shadow-utils vulkan mesa-vulkan-drivers \ - mesa-dri-drivers inotify-tools &>/dev/null + mesa-dri-drivers inotify-tools dbus &>/dev/null fi From 9fe09f2f9564503778142ff96a2de6f5df6b4c91 Mon Sep 17 00:00:00 2001 From: zephyrwc3 Date: Tue, 15 Aug 2023 17:29:47 -0400 Subject: [PATCH 088/121] /usr/bin/python3 instead of env --- blend | 2 +- blend-system | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/blend b/blend index ad8ab7d..36a303d 100755 --- a/blend +++ b/blend @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/python3 # Copyright (C) 2023 Rudra Saraswat # # This file is part of blend. diff --git a/blend-system b/blend-system index 2591373..4fbf012 100755 --- a/blend-system +++ b/blend-system @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/python3 # Copyright (C) 2023 Rudra Saraswat # # This file is part of blend. From cbf034bd62af493bc5bce365068c29a7478fa194 Mon Sep 17 00:00:00 2001 From: zephyrwc3 Date: Tue, 15 Aug 2023 22:32:39 -0400 Subject: [PATCH 089/121] blend-files to /usr/bin/python3 shebang --- blend-files | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blend-files b/blend-files index 1e04e61..6bbe00c 100755 --- a/blend-files +++ b/blend-files @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/python3 import os import subprocess From 8d8ed1cd8749fcee6b33a9a58f85c566b8b885fa Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Thu, 16 Nov 2023 08:08:18 +0000 Subject: [PATCH 090/121] chore: update source --- PKGBUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PKGBUILD b/PKGBUILD index f21d87c..4b5c3a2 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -9,7 +9,7 @@ arch=('x86_64' 'i686') url="https://github.com/blend-os/blend" license=('GPL3') makedepends=("electron" 'git' 'npm' 'base-devel') -source=('git+file://[BASE_ASSEMBLE_PATH]/projects/blend' +source=('git+https://github.com/blend-os/blend' 'blend-settings.desktop' 'blend-package-installer.desktop' 'blend-settings' From 0413532282e595d4dab0523f6b7aaaeb6223faf4 Mon Sep 17 00:00:00 2001 From: "Rudra B.S." Date: Fri, 9 Feb 2024 15:57:30 +0530 Subject: [PATCH 091/121] feat: do not require running containers --- blend | 14 +++++++++----- blend-settings/src/index.html | 6 +++--- blend-settings/src/pages/system.html | 12 ++---------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/blend b/blend index 36a303d..4328181 100755 --- a/blend +++ b/blend @@ -136,7 +136,7 @@ def check_container_status(name): return host_get_output("podman inspect --type container " + name + " --format \"{{.State.Status}}\"") -def core_start_container(name): +def core_start_container(name, new_container=False): subprocess.call(['podman', 'start', name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) @@ -161,6 +161,10 @@ def core_create_container(): distro = args.distro info(f'creating container {name}, using {distro}') + if check_container(name): + error(f'container {name} already exists') + exit(1) + podman_command = [] # Basic stuff @@ -213,9 +217,6 @@ def core_create_container(): ret = subprocess.run(podman_command).returncode if ret != 0: - if check_container(name): - error(f'container {name} already exists') - exit(1) error(f'failed to create container {name}') exit(1) @@ -381,6 +382,9 @@ def update_blends(): def enter_container(): + if check_container_status(args.container_name) != 'running': + core_start_container(args.container_name) + podman_args = ['--env', 'LC_ALL=C.UTF-8'] sudo = [] if os.environ.get('SUDO_USER') == None: @@ -461,7 +465,7 @@ if os.geteuid() == 0 and os.environ['BLEND_ALLOW_ROOT'] == None: description = f''' {colors.bold}{colors.fg.purple}Version:{colors.reset} {__version}{colors.bold} -Use the 'blendOS Settings' app to create and manage Linux containers, Android apps and immutability configuration. +Use the 'blendOS Settings' app to create and manage Linux containers, Android apps and for system configuration. You can install and submit web apps from the Web Store. ''' diff --git a/blend-settings/src/index.html b/blend-settings/src/index.html index bef71a3..924082d 100644 --- a/blend-settings/src/index.html +++ b/blend-settings/src/index.html @@ -1,7 +1,7 @@ - Settings + Linux Containers @@ -18,7 +18,7 @@ Containers - +
@@ -75,4 +75,4 @@ - \ No newline at end of file + diff --git a/blend-settings/src/pages/system.html b/blend-settings/src/pages/system.html index a7d0525..5476f40 100644 --- a/blend-settings/src/pages/system.html +++ b/blend-settings/src/pages/system.html @@ -1,6 +1,6 @@
- System Settings + System Updates
@@ -22,16 +22,8 @@
-
-
-

- You can install packages just like you would on a regular Arch system, or install them in containers. -

-
-
- - \ No newline at end of file + From d467fe4fea34d2a2c2e0e399316bdd0841777f4f Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 9 Feb 2024 21:14:41 +0530 Subject: [PATCH 092/121] feat: add UI for v4 updates --- blend-settings/src/index.html | 2 +- blend-settings/src/internal/js/system.js | 135 +++++++++++++++-------- blend-settings/src/pages/system.html | 9 +- 3 files changed, 95 insertions(+), 51 deletions(-) diff --git a/blend-settings/src/index.html b/blend-settings/src/index.html index 924082d..d66f49c 100644 --- a/blend-settings/src/index.html +++ b/blend-settings/src/index.html @@ -1,7 +1,7 @@ - Linux Containers + System Settings diff --git a/blend-settings/src/internal/js/system.js b/blend-settings/src/internal/js/system.js index 2f53ad6..ef7f3c5 100644 --- a/blend-settings/src/internal/js/system.js +++ b/blend-settings/src/internal/js/system.js @@ -1,53 +1,96 @@ - -function check_app_grouping() { - if (require('fs').existsSync(`${require('os').homedir()}/.config/categorize_apps_gnome_disable`)) { - document.getElementById('app-grouping-toggle').setAttribute('checked', '') +function update_system() { + let start_update_worker = new Worker( + `data:text/javascript, + let s = require('child_process').spawnSync('pkexec', ['systemctl', 'start', 'akshara-system-update']).status + if (s === 0) { + postMessage('success') + } else { + postMessage('failure') + } + ` + ) + start_update_worker.onmessage = e => { + if (e.data == 'success') { + document.getElementById('update-btn').textContent = 'Updating...' + document.getElementById('update-btn').disabled = true + } } } -check_app_grouping() - -$('#app-grouping-toggle').on('change', () => { - if (!document.getElementById('app-grouping-toggle').checked) { - let enable_autogrouping_worker = new Worker( - `data:text/javascript, - let s = require('child_process').spawnSync('rm', ['-f', '${require('os').homedir()}/.config/categorize_apps_gnome_disable']).status - if (s === 0) { - postMessage('success') - } else { - postMessage('failure') - } - ` - ) - enable_autogrouping_worker.onmessage = e => { - if (e.data == 'success') { - document.getElementById('app-grouping-toggle').checked = false - } else { - document.getElementById('app-grouping-toggle').checked = true - } +function check_system_update() { + let start_update_worker = new Worker( + `data:text/javascript, + let s = require('child_process').spawnSync('systemctl', ['is-active', '--quiet', 'akshara-system-update']).status + if (s === 0) { + postMessage('success') + } else { + postMessage('failure') } - } else { - let disable_autogrouping_worker = new Worker( - `data:text/javascript, - require('child_process').spawnSync('mkdir', ['-p', '${require('os').homedir()}/.config']).status - let s = require('child_process').spawnSync('touch', ['${require('os').homedir()}/.config/categorize_apps_gnome_disable']).status - if (s === 0) { - postMessage('success') - } else { - postMessage('failure') - } - ` - ) - disable_autogrouping_worker.onmessage = e => { - if (e.data == 'success') { - document.getElementById('app-grouping-toggle').checked = true - } else { - document.getElementById('app-grouping-toggle').checked = false - } + ` + ) + start_update_worker.onmessage = e => { + if (e.data == 'success') { + document.getElementById('update-btn').textContent = 'Updating...' + document.getElementById('update-btn').disabled = true + } else { + document.getElementById('update-btn').textContent = 'Update' + document.getElementById('update-btn').disabled = false } } -}); +} -if (require('process').env.XDG_CURRENT_DESKTOP.includes('GNOME')) { - $('#app-grouping-item').removeClass('d-none') -} \ No newline at end of file +check_system_update() +setInterval(check_system_update, 5000) + +// function check_app_grouping() { +// if (require('fs').existsSync(`${require('os').homedir()}/.config/categorize_apps_gnome_disable`)) { +// document.getElementById('app-grouping-toggle').setAttribute('checked', '') +// } +// } + +// check_app_grouping() + +// $('#app-grouping-toggle').on('change', () => { +// if (!document.getElementById('app-grouping-toggle').checked) { +// let enable_autogrouping_worker = new Worker( +// `data:text/javascript, +// let s = require('child_process').spawnSync('rm', ['-f', '${require('os').homedir()}/.config/categorize_apps_gnome_disable']).status +// if (s === 0) { +// postMessage('success') +// } else { +// postMessage('failure') +// } +// ` +// ) +// enable_autogrouping_worker.onmessage = e => { +// if (e.data == 'success') { +// document.getElementById('app-grouping-toggle').checked = false +// } else { +// document.getElementById('app-grouping-toggle').checked = true +// } +// } +// } else { +// let disable_autogrouping_worker = new Worker( +// `data:text/javascript, +// require('child_process').spawnSync('mkdir', ['-p', '${require('os').homedir()}/.config']).status +// let s = require('child_process').spawnSync('touch', ['${require('os').homedir()}/.config/categorize_apps_gnome_disable']).status +// if (s === 0) { +// postMessage('success') +// } else { +// postMessage('failure') +// } +// ` +// ) +// disable_autogrouping_worker.onmessage = e => { +// if (e.data == 'success') { +// document.getElementById('app-grouping-toggle').checked = true +// } else { +// document.getElementById('app-grouping-toggle').checked = false +// } +// } +// } +// }); + +// if (require('process').env.XDG_CURRENT_DESKTOP.includes('GNOME')) { +// $('#app-grouping-item').removeClass('d-none') +// } \ No newline at end of file diff --git a/blend-settings/src/pages/system.html b/blend-settings/src/pages/system.html index 5476f40..653f17b 100644 --- a/blend-settings/src/pages/system.html +++ b/blend-settings/src/pages/system.html @@ -3,17 +3,18 @@ System Updates
-
+
- Disable app grouping + Update system

- Do not automatically group apps of different categories/web apps/Android apps. + Checks for updates, and updates your computer if they're available.

- +
From 3d98a0f04bc02343f1f52c0d8f6e4889923e729c Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 9 Feb 2024 21:16:26 +0530 Subject: [PATCH 093/121] chore: rename to 'System' --- blend-settings.desktop | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/blend-settings.desktop b/blend-settings.desktop index 8137fd9..4f5cad6 100644 --- a/blend-settings.desktop +++ b/blend-settings.desktop @@ -1,9 +1,10 @@ [Desktop Entry] -Name=blendOS Settings +Name=System Exec=blend-settings %U Terminal=false Type=Application Icon=blend-settings StartupWMClass=blend-settings Comment=Settings for blendOS. +Keywords=Settings; Categories=System; From 30bce4aace6395cb8ca41d61f0d406a82aadcf9d Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 9 Feb 2024 21:45:59 +0530 Subject: [PATCH 094/121] feat: focus exclusively on core distros --- blend | 19 +++++++------------ blend-settings/src/pages/containers.html | 10 ++-------- init-blend | 24 +----------------------- 3 files changed, 10 insertions(+), 43 deletions(-) diff --git a/blend b/blend index 4328181..88b4424 100755 --- a/blend +++ b/blend @@ -87,19 +87,14 @@ def error(err): distro_map = { - 'arch': 'docker.io/library/archlinux', - 'almalinux-9': 'quay.io/almalinux/almalinux:9', - 'crystal-linux': 'registry.getcryst.al/crystal/misc/docker:latest', - 'debian': 'docker.io/library/debian:latest', - 'fedora-38': 'docker.io/library/fedora:38', - 'kali-linux': 'docker.io/kalilinux/kali-rolling', - 'neurodebian-bookworm': 'docker.io/library/neurodebian:nd120', - 'rocky-linux': 'docker.io/rockylinux/rockylinux:9', - 'ubuntu-22.04': 'docker.io/library/ubuntu:22.04', - 'ubuntu-23.04': 'docker.io/library/ubuntu:23.04' + 'arch-linux': 'docker.io/library/archlinux', + 'debian': 'quay.io/toolbx-images/debian-toolbox:testing', + 'fedora-39': 'registry.fedoraproject.org/fedora-toolbox:39', + 'centos': 'quay.io/toolbx-images/centos-toolbox:latest', + 'ubuntu-22.04': 'quay.io/toolbx/ubuntu-toolbox:22.04', } -default_distro = 'arch' +default_distro = 'arch-linux' def get_distro(): @@ -152,7 +147,7 @@ def core_start_container(name, new_container=False): 'podman', args=['logs', '-f', '--since', str(start_time), name], timeout=300) logproc.logfile_read = sys.stdout.buffer - logproc.expect('Completed container setup.') + logproc.expect('Started container.') logproc.terminate() diff --git a/blend-settings/src/pages/containers.html b/blend-settings/src/pages/containers.html index d7bf67b..dccb3ba 100644 --- a/blend-settings/src/pages/containers.html +++ b/blend-settings/src/pages/containers.html @@ -10,16 +10,10 @@
diff --git a/init-blend b/init-blend index ed00527..0b71ab2 100755 --- a/init-blend +++ b/init-blend @@ -62,29 +62,7 @@ while true; do esac done -cat << 'EOF' - - - ▄▄▄▄ ██▓ ▓█████ ███▄ █ ▓█████▄ -▓█████▄ ▓██▒ ▓█ ▀ ██ ▀█ █ ▒██▀ ██▌ -▒██▒ ▄██▒██░ ▒███ ▓██ ▀█ ██▒░██ █▌ -▒██░█▀ ▒██░ ▒▓█ ▄ ▓██▒ ▐▌██▒░▓█▄ ▌ -░▓█ ▀█▓░██████▒░▒████▒▒██░ ▓██░░▒████▓ -░▒▓███▀▒░ ▒░▓ ░░░ ▒░ ░░ ▒░ ▒ ▒ ▒▒▓ ▒ -▒░▒ ░ ░ ░ ▒ ░ ░ ░ ░░ ░░ ░ ▒░ ░ ▒ ▒ - ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ - ░ ░ ░ ░ ░ ░ ░ - ░ ░ - -=================== - Credits -=================== - -* NVIDIA driver support - Luca Di Maio (from Distrobox) -EOF - -echo -echo 'Starting blend... (this may take a few minutes)' +echo 'Starting container... (this may take a few minutes)' echo bmount() { From 6589caf322a5219b8f5249625b550209939da9a8 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 9 Feb 2024 21:46:29 +0530 Subject: [PATCH 095/121] feat: add CentOS --- blend-settings/src/pages/containers.html | 1 + 1 file changed, 1 insertion(+) diff --git a/blend-settings/src/pages/containers.html b/blend-settings/src/pages/containers.html index dccb3ba..54806c2 100644 --- a/blend-settings/src/pages/containers.html +++ b/blend-settings/src/pages/containers.html @@ -12,6 +12,7 @@ From db553e0199c53ede1c05ded01b273b51d8912abd Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 9 Feb 2024 21:47:55 +0530 Subject: [PATCH 096/121] chore: update checksums --- PKGBUILD | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/PKGBUILD b/PKGBUILD index 4b5c3a2..475734e 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -15,9 +15,11 @@ source=('git+https://github.com/blend-os/blend' 'blend-settings' 'blend.sh') sha256sums=('SKIP' + 'ba11ef22fe92a78239855c1bbc07d8c5be7cd94728bb3baf6184d2f42a80a4c2' 'a605d24d2fa7384b45a94105143db216db1ffc0bdfc7f6eec758ef2026e61e54' '23decd858ab49e860999bba783da78f43adc7b678065057cadfc2eeaefb2e870' - '73cb7c39190d36f233b8dfbc3e3e6737d56e61e90881ad95f09e5ae1f9b405a8' 'SKIP') + '73cb7c39190d36f233b8dfbc3e3e6737d56e61e90881ad95f09e5ae1f9b405a8' + '7dab67fb5c0239b6645659a7838de85b1420683a5bf52d8a8a3d324b69210a40') pkgver() { cd "${srcdir}/${pkgbase%-git}" From 346e5e5b8bcf1c142b197395639230b203423f26 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 9 Feb 2024 21:51:32 +0530 Subject: [PATCH 097/121] chore: update checksums --- PKGBUILD | 1 - 1 file changed, 1 deletion(-) diff --git a/PKGBUILD b/PKGBUILD index 475734e..7756307 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -16,7 +16,6 @@ source=('git+https://github.com/blend-os/blend' 'blend.sh') sha256sums=('SKIP' 'ba11ef22fe92a78239855c1bbc07d8c5be7cd94728bb3baf6184d2f42a80a4c2' - 'a605d24d2fa7384b45a94105143db216db1ffc0bdfc7f6eec758ef2026e61e54' '23decd858ab49e860999bba783da78f43adc7b678065057cadfc2eeaefb2e870' '73cb7c39190d36f233b8dfbc3e3e6737d56e61e90881ad95f09e5ae1f9b405a8' '7dab67fb5c0239b6645659a7838de85b1420683a5bf52d8a8a3d324b69210a40') From 1840bdab957a0336caa735768c0bde967c06735e Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 9 Feb 2024 23:16:52 +0530 Subject: [PATCH 098/121] chore: update distros in package installer --- blend | 2 +- blend-settings/src/internal/js/android.js | 2 +- blend-settings/src/internal/js/system.js | 7 +++++++ blend-settings/src/package-installer.html | 13 ++++--------- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/blend b/blend index 88b4424..f34eb5f 100755 --- a/blend +++ b/blend @@ -144,7 +144,7 @@ def core_start_container(name, new_container=False): exit(1) logproc = pexpect.spawn( - 'podman', args=['logs', '-f', '--since', str(start_time), name], timeout=300) + 'podman', args=['logs', '-f', '--since', str(start_time), name], timeout=3600) logproc.logfile_read = sys.stdout.buffer logproc.expect('Started container.') diff --git a/blend-settings/src/internal/js/android.js b/blend-settings/src/internal/js/android.js index f999b58..ba2a955 100644 --- a/blend-settings/src/internal/js/android.js +++ b/blend-settings/src/internal/js/android.js @@ -50,7 +50,7 @@ function install_aurora_store() { let aurora_store_worker = new Worker( `data:text/javascript, require('child_process').spawnSync('sh', ['-c', 'mkdir -p ~/.cache/blend-settings; rm -f ~/.cache/blend-settings/aurora.apk']) - let s1 = require('child_process').spawnSync('sh', ['-c', 'wget -O ~/.cache/blend-settings/aurora.apk https://gitlab.com/AuroraOSS/AuroraStore/uploads/bbc1bd5a77ab2b40bbf288ccbef8d1f0/AuroraStore_4.1.1.apk']).status + let s1 = require('child_process').spawnSync('sh', ['-c', 'wget -O ~/.cache/blend-settings/aurora.apk https://auroraoss.com/AuroraStore/Stable/AuroraStore_4.4.1.apk']).status if (s1 != 0) { postMessage('failed') } else { diff --git a/blend-settings/src/internal/js/system.js b/blend-settings/src/internal/js/system.js index ef7f3c5..c47b719 100644 --- a/blend-settings/src/internal/js/system.js +++ b/blend-settings/src/internal/js/system.js @@ -18,6 +18,13 @@ function update_system() { } function check_system_update() { + if (require('fs').existsSync('/.update')) { + document.getElementById('update-btn').onclick = () => { + require('child_process').spawnSync('reboot') + } + document.getElementById('update-btn').textContent = 'Reboot' + document.getElementById('update-btn').disabled = false + } let start_update_worker = new Worker( `data:text/javascript, let s = require('child_process').spawnSync('systemctl', ['is-active', '--quiet', 'akshara-system-update']).status diff --git a/blend-settings/src/package-installer.html b/blend-settings/src/package-installer.html index e70380f..a20640b 100644 --- a/blend-settings/src/package-installer.html +++ b/blend-settings/src/package-installer.html @@ -97,18 +97,14 @@ document.getElementById('source_select').innerHTML = ` - - - ` } else if (package_name.endsWith('.rpm')) { document.getElementById('packaging-format').src = '../static/RPM.svg' package_type = 'rpm' document.getElementById('source_select').innerHTML = ` - - - + + ` } else if (package_name.endsWith('.apk')) { document.getElementById('packaging-format').src = '../static/APK.svg' @@ -117,15 +113,14 @@ require('fs').stat('/var/lib/waydroid/waydroid.prop', (err, stat) => { if (err != null) { - document.getElementById('install-button').outerHTML = "

You'll need to initialize Android app support from the blendOS Settings app first.

" + document.getElementById('install-button').outerHTML = "

You'll need to initialize Android app support from the System app first.

" } }) } else if (package_name.includes('.pkg.tar')) { document.getElementById('packaging-format').src = '../static/PKG.svg' package_type = 'pkg' document.getElementById('source_select').innerHTML = ` - - + ` } From 3194fc8124502552dc2ee6870aceb593c8b0b031 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 9 Feb 2024 23:17:53 +0530 Subject: [PATCH 099/121] chore: add mimetype to blend-package-installer.desktop --- PKGBUILD | 2 +- blend-package-installer.desktop | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/PKGBUILD b/PKGBUILD index 7756307..b83ed43 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -16,7 +16,7 @@ source=('git+https://github.com/blend-os/blend' 'blend.sh') sha256sums=('SKIP' 'ba11ef22fe92a78239855c1bbc07d8c5be7cd94728bb3baf6184d2f42a80a4c2' - '23decd858ab49e860999bba783da78f43adc7b678065057cadfc2eeaefb2e870' + '994bebb5e993130e5cfaac8f9e1e8e676662a0cc76abcf90c8e128576506b818' '73cb7c39190d36f233b8dfbc3e3e6737d56e61e90881ad95f09e5ae1f9b405a8' '7dab67fb5c0239b6645659a7838de85b1420683a5bf52d8a8a3d324b69210a40') diff --git a/blend-package-installer.desktop b/blend-package-installer.desktop index be2702d..e5c33b9 100644 --- a/blend-package-installer.desktop +++ b/blend-package-installer.desktop @@ -5,6 +5,7 @@ Terminal=false NoDisplay=true Type=Application Icon=blend-settings +MimeType=application/vnd.debian.binary-package;application/x-rpm;application/vnd.android.package-archive; StartupWMClass=blend-settings Comment=Package installer for blendOS. Categories=System; From 11bd0c28b4270bac8156aa31629135d013a6a8f9 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 9 Feb 2024 23:24:53 +0530 Subject: [PATCH 100/121] chore: show reboot button on update --- blend-settings/src/internal/js/system.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/blend-settings/src/internal/js/system.js b/blend-settings/src/internal/js/system.js index c47b719..25644ab 100644 --- a/blend-settings/src/internal/js/system.js +++ b/blend-settings/src/internal/js/system.js @@ -22,8 +22,11 @@ function check_system_update() { document.getElementById('update-btn').onclick = () => { require('child_process').spawnSync('reboot') } + document.getElementById('update-btn').textContent = 'Reboot' document.getElementById('update-btn').disabled = false + + return; } let start_update_worker = new Worker( `data:text/javascript, From 9d93042e1f91bd0fda7af6456b35097de9a3169d Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 9 Feb 2024 23:50:48 +0530 Subject: [PATCH 101/121] fix: logproc term string --- init-blend | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init-blend b/init-blend index 0b71ab2..ecad72e 100755 --- a/init-blend +++ b/init-blend @@ -386,7 +386,7 @@ for full_file in /usr/share/applications/*.desktop; do chmod 755 "${HOME}/.local/share/applications/blend;${CONTAINER_NAME};${file}" done -echo "Completed container setup." +echo "Started container." mkdir -p /usr/share/applications /usr/bin inotifywait -m /usr/share/applications /usr/bin -e create,delete,move 2>/dev/null | From f1bf3d7e1dd03016667495926df902ed23376987 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sat, 10 Feb 2024 10:24:51 +0530 Subject: [PATCH 102/121] feat: allow running binaries with sudo --- blend | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/blend b/blend index f34eb5f..b7fc1ab 100755 --- a/blend +++ b/blend @@ -128,11 +128,17 @@ def check_container(name): def check_container_status(name): - return host_get_output("podman inspect --type container " + name + " --format \"{{.State.Status}}\"") + if os.environ.get('SUDO_USER') == None: + return host_get_output("podman inspect --type container " + name + " --format \"{{.State.Status}}\"") + else: + return host_get_output(f"sudo -u {os.environ.get('SUDO_USER')} podman inspect --type container " + name + " --format \"{{.State.Status}}\"") def core_start_container(name, new_container=False): - subprocess.call(['podman', 'start', name], + sudo = [] + if os.environ.get('SUDO_USER') != None: + sudo = ['sudo', '-u', os.environ.get('SUDO_USER')] + subprocess.call([*sudo, 'podman', 'start', name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) start_time = time.time() - 1000 # workaround From d1baddc06d8e20021493f260a2315ab01cf770b7 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Sun, 11 Feb 2024 12:36:19 +0530 Subject: [PATCH 103/121] fix: SUDO_USER not used when starting container --- blend | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/blend b/blend index b7fc1ab..570a068 100755 --- a/blend +++ b/blend @@ -149,8 +149,12 @@ def core_start_container(name, new_container=False): subprocess.call(['podman', 'logs', '--since', str(start_time), name]) exit(1) - logproc = pexpect.spawn( - 'podman', args=['logs', '-f', '--since', str(start_time), name], timeout=3600) + if os.environ.get('SUDO_USER') == None: + logproc = pexpect.spawn( + 'podman', args=['logs', '-f', '--since', str(start_time), name], timeout=3600) + else: + logproc = pexpect.spawn( + 'sudo', args=['-u', os.environ.get('SUDO_USER'), 'podman', 'logs', '-f', '--since', str(start_time), name], timeout=3600) logproc.logfile_read = sys.stdout.buffer logproc.expect('Started container.') From 25034878c3b6979742995b5ae72efc7717699e56 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Mon, 3 Jun 2024 17:28:15 +0000 Subject: [PATCH 104/121] feat: include user utility --- PKGBUILD | 1 + 1 file changed, 1 insertion(+) diff --git a/PKGBUILD b/PKGBUILD index b83ed43..fdfc71e 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -51,6 +51,7 @@ package_blend-git() { cd "${srcdir}/${pkgbase%-git}" install -Dm755 \ "${pkgname%-git}" \ + "user" \ "init-${pkgname%-git}" \ "host-${pkgname%-git}" \ "${pkgname%-git}-files" \ From 90bf64f87a3de373cdb27434615f077cdb874299 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Mon, 3 Jun 2024 18:48:46 +0000 Subject: [PATCH 105/121] feat: include a stripped-down version of the user utility --- user | 252 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100755 user diff --git a/user b/user new file mode 100755 index 0000000..b6624f0 --- /dev/null +++ b/user @@ -0,0 +1,252 @@ +#!/usr/bin/python3 + +import os +import yaml +import click +import subprocess + +from urllib.request import urlopen + + +class colors: + reset = '\033[0m' + bold = '\033[01m' + disable = '\033[02m' + underline = '\033[04m' + reverse = '\033[07m' + strikethrough = '\033[09m' + invisible = '\033[08m' + + class fg: + black = '\033[30m' + red = '\033[31m' + green = '\033[32m' + orange = '\033[33m' + blue = '\033[34m' + purple = '\033[35m' + cyan = '\033[36m' + lightgrey = '\033[37m' + darkgrey = '\033[90m' + lightred = '\033[91m' + lightgreen = '\033[92m' + yellow = '\033[93m' + lightblue = '\033[94m' + pink = '\033[95m' + lightcyan = '\033[96m' + + rainbow = [lightred, orange, yellow, + lightgreen, lightcyan, blue, purple] + seq = 0 + + def random(self): + if self.seq == 7: + self.seq = 0 + self.seq += 1 + return self.rainbow[self.seq - 1] + + def clear_seq(self): + self.seq = 0 + + class bg: + black = '\033[40m' + red = '\033[41m' + green = '\033[42m' + orange = '\033[43m' + blue = '\033[44m' + purple = '\033[45m' + cyan = '\033[46m' + lightgrey = '\033[47m' + + +fg = colors.fg() + + +def info(msg): + print(colors.bold + fg.cyan + '[INFO] ' + + colors.reset + msg + colors.reset) + + +def print_list(msg): + print(colors.bold + fg.random() + '[LIST] ' + + colors.reset + msg + colors.reset) + + +def modrun(msg): + print(colors.bold + fg.green + '[MODRUN] ' + + colors.reset + msg + colors.reset) + + +def container_msg(msg): + print(colors.bold + fg.purple + '[CONTAINER] ' + + colors.reset + msg + colors.reset) + + +def association_msg(msg): + print(colors.bold + fg.random() + '[ASSOCIATION] ' + + colors.reset + msg + colors.reset) + + +def warn(warning): + print(colors.bold + fg.yellow + '[WARNING] ' + + colors.reset + warning + colors.reset) + + +def error(err): + print(colors.bold + fg.red + '[ERROR] ' + + colors.reset + err + colors.reset) + + +def proceed(): + print(colors.bold + fg.red + '[QUESTION] ' + + colors.reset + 'would you like to proceed?' + colors.reset) + info(f'(press {colors.bold}ENTER{colors.reset} to proceed, or {colors.bold}^C{colors.reset}/{colors.bold}^D{colors.reset} to cancel)') + input() + + +@click.group("cli") +def cli(): + """Manage user operations using the user utility on blendOS.""" + + +def main(): + cli(prog_name="user") + + +@cli.command("associate") +@click.argument('association') +@click.argument('container') +def associate_binary(association, container): + ''' + Create an association (for example, apt -> ubuntu) + ''' + + if not os.path.exists(os.path.expanduser(f'~/.local/bin/blend_bin/{association}.{container}')): + error(f'{colors.bold}{association}.{container}{colors.reset} does not exist') + exit() + if os.path.isfile(os.path.expanduser('~/.local/bin/blend_bin/.associations')): + subprocess.run(['sed', '-i', f's/^{association}\\x0//g', + os.path.expanduser('~/.local/bin/blend_bin/.associations')]) + with open(os.path.expanduser('~/.local/bin/blend_bin/.associations'), 'a+') as f: + f.write(f'{association}\0{container}\n') + _exists = os.path.exists(os.path.expanduser( + f'~/.local/bin/blend_bin/{association}')) + subprocess.run(['ln', '-sf', f'{association}.{container}', + os.path.expanduser(f'~/.local/bin/blend_bin/{association}')]) + association_msg(('modified' if _exists else 'created') + + f' {colors.bold}{association} -> {container}{colors.reset}') + + +@cli.command("dissociate") +@click.argument('association') +def associate_binary(association): + ''' + Remove an association + ''' + + if not os.path.exists(os.path.expanduser(f'~/.local/bin/blend_bin/{association}')): + error(f'{colors.bold}{association}{colors.reset} does not exist') + exit() + if os.path.isfile(os.path.expanduser('~/.local/bin/blend_bin/.associations')): + subprocess.run(['sed', '-i', f's/^{association}\\x0//g', + os.path.expanduser('~/.local/bin/blend_bin/.associations')]) + subprocess.run( + ['rm', '-f', os.path.expanduser(f'~/.local/bin/blend_bin/{association}')]) + association_msg(f'dissociated {colors.bold}{association}') + + +@cli.command("create-container") +@click.argument('container_name') +@click.argument('distro', default='arch') +def create_container(container_name, distro): + ''' + Create a container + ''' + if distro not in ('arch', 'almalinux-9', 'crystal-linux', 'debian', 'fedora-38', 'kali-linux', 'neurodebian-bookworm', 'rocky-linux', 'ubuntu-22.04', 'ubuntu-23.04'): + error( + f'distro {colors.bold}{distro}{colors.reset} not supported') + if subprocess.run(['podman', 'container', 'exists', container_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0: + error(f'container {colors.bold}{container_name}{colors.reset} already exists') + exit(1) + subprocess.run(['blend', 'create-container', '-cn', container_name, '-d', distro]) + + +@cli.command("delete-container") +@click.argument('container') +def delete_container(container): + ''' + Delete a container + ''' + if subprocess.run(['podman', 'container', 'exists', container], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0: + error(f'container {colors.bold}{container}{colors.reset} does not exist') + exit(1) + subprocess.run(['blend', 'remove-container', container]) + + +@cli.command("shell") +@click.argument('container') +def shell(container): + ''' + Enter a shell inside a container + ''' + if subprocess.run(['podman', 'container', 'exists', container], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0: + error(f'container {colors.bold}{container}{colors.reset} does not exist') + exit(1) + creation_env = os.environ.copy() + creation_env['BLEND_NO_CHECK'] = 'true' + subprocess.run(['blend', 'enter', '-cn', container], env=creation_env) + + +@cli.command("exec") +@click.argument('container') +@click.argument('cmds', nargs=-1, required=True) +def exec_c(container, cmds): + ''' + Run a command inside a container + ''' + if subprocess.run(['podman', 'container', 'exists', container], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0: + error(f'container {colors.bold}{container}{colors.reset} does not exist') + exit(1) + creation_env = os.environ.copy() + creation_env['BLEND_NO_CHECK'] = 'true' + subprocess.run(['blend', 'enter', '-cn', container, '--', *cmds], env=creation_env) + + +@cli.command("install") +@click.argument('container') +@click.argument('pkgs', nargs=-1, required=True) +def install_c(container, pkgs): + ''' + Install a package inside a container + ''' + if os.path.isfile(os.path.expanduser(f'~/.local/bin/blend_bin/apt.{container}')): + subprocess.run([f'sudo.{container}', 'apt', 'update']) + subprocess.run([f'sudo.{container}', 'apt', 'install', *pkgs]) + elif os.path.isfile(os.path.expanduser(f'~/.local/bin/blend_bin/dnf.{container}')): + subprocess.run([f'sudo.{container}', 'dnf', 'install', *pkgs]) + elif os.path.isfile(os.path.expanduser(f'~/.local/bin/blend_bin/pacman.{container}')): + subprocess.run([f'sudo.{container}', 'pacman', '-Syu', *pkgs]) + else: + error(f'container {colors.bold}{container}{colors.reset} does not exist') + exit(1) + + +@cli.command("remove") +@click.argument('container') +@click.argument('pkgs', nargs=-1, required=True) +def remove_c(container, pkgs): + ''' + Remove a package inside a container + ''' + if os.path.isfile(os.path.expanduser(f'~/.local/bin/blend_bin/apt.{container}')): + subprocess.run([f'sudo.{container}', 'apt', 'purge', *pkgs]) + elif os.path.isfile(os.path.expanduser(f'~/.local/bin/blend_bin/dnf.{container}')): + subprocess.run([f'sudo.{container}', 'dnf', 'remove', *pkgs]) + elif os.path.isfile(os.path.expanduser(f'~/.local/bin/blend_bin/pacman.{container}')): + subprocess.run([f'sudo.{container}', 'pacman', '-Rcns', *pkgs]) + else: + error(f'container {colors.bold}{container}{colors.reset} does not exist') + exit(1) + + +if __name__ == '__main__': + main() From 96a25b7c17be4e72293937bb71f803ff92456f04 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Mon, 3 Jun 2024 20:16:59 +0000 Subject: [PATCH 106/121] feat: add python-click as dep --- PKGBUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PKGBUILD b/PKGBUILD index fdfc71e..bbb88db 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -44,7 +44,7 @@ build() { } package_blend-git() { - depends=('bash' 'blend-settings-git' 'podman' 'python' 'python-pexpect') + depends=('bash' 'blend-settings-git' 'podman' 'python' 'python-click' 'python-pexpect') provides=("${pkgname%-git}") conflicts=("${pkgname%-git}") From 5115de41c19e3b3c4452385b232ac0f8579667d8 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Wed, 5 Jun 2024 22:30:44 +0000 Subject: [PATCH 107/121] feat: change aurora store download link --- blend-settings/src/internal/js/android.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blend-settings/src/internal/js/android.js b/blend-settings/src/internal/js/android.js index ba2a955..ae216b3 100644 --- a/blend-settings/src/internal/js/android.js +++ b/blend-settings/src/internal/js/android.js @@ -50,7 +50,7 @@ function install_aurora_store() { let aurora_store_worker = new Worker( `data:text/javascript, require('child_process').spawnSync('sh', ['-c', 'mkdir -p ~/.cache/blend-settings; rm -f ~/.cache/blend-settings/aurora.apk']) - let s1 = require('child_process').spawnSync('sh', ['-c', 'wget -O ~/.cache/blend-settings/aurora.apk https://auroraoss.com/AuroraStore/Stable/AuroraStore_4.4.1.apk']).status + let s1 = require('child_process').spawnSync('sh', ['-c', 'wget -O ~/.cache/blend-settings/aurora.apk https://f-droid.org/repo/com.aurora.store_58.apk']).status if (s1 != 0) { postMessage('failed') } else { @@ -194,4 +194,4 @@ $('#automatic-state-toggle').on('change', () => { } } } -}); \ No newline at end of file +}); From 64e9902f6e471600ffdcbbb6fcd3198a5237272b Mon Sep 17 00:00:00 2001 From: askiiart Date: Tue, 1 Oct 2024 22:37:25 -0500 Subject: [PATCH 108/121] edit for my version --- PKGBUILD | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index bbb88db..a79c7d1 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -6,10 +6,10 @@ pkgver=r50.2e0016f pkgrel=1 pkgdesc="A package manager for blendOS" arch=('x86_64' 'i686') -url="https://github.com/blend-os/blend" +url="https://git.askiiart.net/askiiart-blendos/blend" license=('GPL3') makedepends=("electron" 'git' 'npm' 'base-devel') -source=('git+https://github.com/blend-os/blend' +source=('git+https://git.askiiart.net/askiiart-blendos/blend' 'blend-settings.desktop' 'blend-package-installer.desktop' 'blend-settings' From e47c5d7df79c3da42f19d2fe3776079fe1223e88 Mon Sep 17 00:00:00 2001 From: ALPERDURUKAN Date: Mon, 7 Oct 2024 21:15:48 +0300 Subject: [PATCH 109/121] Update blend Added Ubuntu 24.04 LTS to containers --- blend | 1 + 1 file changed, 1 insertion(+) diff --git a/blend b/blend index 570a068..0bc2eb5 100755 --- a/blend +++ b/blend @@ -92,6 +92,7 @@ distro_map = { 'fedora-39': 'registry.fedoraproject.org/fedora-toolbox:39', 'centos': 'quay.io/toolbx-images/centos-toolbox:latest', 'ubuntu-22.04': 'quay.io/toolbx/ubuntu-toolbox:22.04', + 'ubuntu-24.04-lts': 'quay.io/toolbx/ubuntu-toolbox:24.04', } default_distro = 'arch-linux' From a890b119d7ec01f8f6ad8f7ba83110a6d7fb7efd Mon Sep 17 00:00:00 2001 From: ALPERDURUKAN Date: Mon, 7 Oct 2024 21:18:50 +0300 Subject: [PATCH 110/121] Update containers.html Added Ubuntu 24.04 LTS container support --- blend-settings/src/pages/containers.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/blend-settings/src/pages/containers.html b/blend-settings/src/pages/containers.html index 54806c2..935a85a 100644 --- a/blend-settings/src/pages/containers.html +++ b/blend-settings/src/pages/containers.html @@ -15,6 +15,7 @@ +
@@ -66,4 +67,4 @@ - \ No newline at end of file + From f7c5507d923403eb952be098e4529970c3ec312e Mon Sep 17 00:00:00 2001 From: askiiart Date: Tue, 8 Apr 2025 15:27:57 -0500 Subject: [PATCH 111/121] allow arguments for commands running in container (allow escaping hyphens) --- user | 1 + 1 file changed, 1 insertion(+) diff --git a/user b/user index b6624f0..ed8d6a4 100755 --- a/user +++ b/user @@ -208,6 +208,7 @@ def exec_c(container, cmds): exit(1) creation_env = os.environ.copy() creation_env['BLEND_NO_CHECK'] = 'true' + cmds = [ cmd.replace('\\-', '-') for cmd in cmds] subprocess.run(['blend', 'enter', '-cn', container, '--', *cmds], env=creation_env) From 4732a5735d61c3e9d9ddcfcc5731a4fa94d460e1 Mon Sep 17 00:00:00 2001 From: askiiart Date: Tue, 8 Apr 2025 15:31:39 -0500 Subject: [PATCH 112/121] misc fixes for distros/images not matching up right * fix arch/arch-linux mismatch * fix expecting fedora-rawhide (now fedora-*) * fix inconsistency in ubuntu naming * add aliases for arch and ubuntu to avoid breakage --- blend | 55 ++++++++++++++++++++++++++++++++++++++----------------- user | 11 ++++++----- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/blend b/blend index 0bc2eb5..d34eda5 100755 --- a/blend +++ b/blend @@ -92,7 +92,7 @@ distro_map = { 'fedora-39': 'registry.fedoraproject.org/fedora-toolbox:39', 'centos': 'quay.io/toolbx-images/centos-toolbox:latest', 'ubuntu-22.04': 'quay.io/toolbx/ubuntu-toolbox:22.04', - 'ubuntu-24.04-lts': 'quay.io/toolbx/ubuntu-toolbox:24.04', + 'ubuntu-24.04': 'quay.io/toolbx/ubuntu-toolbox:24.04', } default_distro = 'arch-linux' @@ -168,8 +168,8 @@ def core_create_container(): info(f'creating container {name}, using {distro}') if check_container(name): - error(f'container {name} already exists') - exit(1) + error(f'container {name} already exists') + exit(1) podman_command = [] @@ -248,12 +248,15 @@ def core_run_container(cmd): def core_install_pkg(pkg): - if args.distro == 'fedora-rawhide': + if args.distro == 'arch': + args.distro = 'arch-linux' + + if args.distro.startswith('fedora-'): if args.noconfirm == True: core_run_container(f'sudo dnf -y install {pkg}') else: core_run_container(f'sudo dnf install {pkg}') - elif args.distro == 'arch': + elif args.distro == 'arch-linux': if core_get_retcode('[ -f /usr/bin/yay ]') != 0: core_run_container('sudo pacman -Sy') core_run_container( @@ -274,12 +277,15 @@ def core_install_pkg(pkg): def core_remove_pkg(pkg): - if args.distro == 'fedora-rawhide': + if args.distro == 'arch': + args.distro = 'arch-linux' + + if args.distro.startswith('fedora-'): if args.noconfirm == True: core_run_container(f'sudo dnf -y remove {pkg}') else: core_run_container(f'sudo dnf remove {pkg}') - elif args.distro == 'arch': + elif args.distro == 'arch-linux': if args.noconfirm == True: core_run_container(f'sudo pacman --noconfirm -Rcns {pkg}') else: @@ -293,9 +299,12 @@ def core_remove_pkg(pkg): def core_search_pkg(pkg): - if args.distro == 'fedora-rawhide': + if args.distro == 'arch': + args.distro = 'arch-linux' + + if args.distro.startswith('fedora-'): core_run_container(f'dnf search {pkg}') - elif args.distro == 'arch': + elif args.distro == 'arch-linux': core_run_container(f'yay -Sy') core_run_container(f'yay {pkg}') elif args.distro.startswith('ubuntu-'): @@ -304,9 +313,12 @@ def core_search_pkg(pkg): def core_show_pkg(pkg): - if args.distro == 'fedora-rawhide': + if args.distro == 'arch': + args.distro = 'arch-linux' + + if args.distro.startswith('fedora-'): core_run_container(f'dnf info {pkg}') - elif args.distro == 'arch': + elif args.distro == 'arch-linux': core_run_container(f'yay -Sy') core_run_container(f'yay -Si {pkg}') elif args.distro.startswith('ubuntu-'): @@ -358,21 +370,27 @@ def show_blend(): def sync_blends(): - if args.distro == 'fedora-rawhide': + if args.distro == 'arch': + args.distro = 'arch-linux' + + if args.distro.startswith('fedora-'): core_run_container(f'dnf makecache') - elif args.distro == 'arch': + elif args.distro == 'arch-linux': core_run_container(f'yay -Syy') elif args.distro.startswith('ubuntu-'): core_run_container(f'sudo apt-get update') def update_blends(): - if args.distro == 'fedora-rawhide': + if args.distro == 'arch': + args.distro = 'arch-linux' + + if args.distro.startswith('fedora-'): if args.noconfirm == True: core_run_container(f'sudo dnf -y upgrade') else: core_run_container(f'sudo dnf upgrade') - elif args.distro == 'arch': + elif args.distro == 'arch-linux': if args.noconfirm == True: core_run_container(f'yay --noconfirm') else: @@ -428,6 +446,7 @@ def enter_container(): def create_container(): for container in args.pkg: + container = 'ubuntu-24.04' if container == 'ubuntu-24.04-lts' else container args.container_name = container if container in distro_map.keys() and distro_input == None: args.distro = container @@ -443,10 +462,12 @@ def remove_container(): stdout=subprocess.DEVNULL) for bin in os.listdir(os.path.expanduser('~/.local/bin/blend_bin')): if bin.endswith(f'.{container}'): - os.remove(os.path.join(os.path.expanduser('~/.local/bin/blend_bin'), bin)) + os.remove(os.path.join(os.path.expanduser( + '~/.local/bin/blend_bin'), bin)) for app in os.listdir(os.path.expanduser('~/.local/share/applications')): if app.startswith(f'blend;{container};'): - os.remove(os.path.join(os.path.expanduser('~/.local/share/applications'), app)) + os.remove(os.path.join(os.path.expanduser( + '~/.local/share/applications'), app)) def start_containers(): diff --git a/user b/user index ed8d6a4..98b93cd 100755 --- a/user +++ b/user @@ -156,18 +156,19 @@ def associate_binary(association): @cli.command("create-container") @click.argument('container_name') -@click.argument('distro', default='arch') +@click.argument('distro', required=False) def create_container(container_name, distro): ''' Create a container ''' - if distro not in ('arch', 'almalinux-9', 'crystal-linux', 'debian', 'fedora-38', 'kali-linux', 'neurodebian-bookworm', 'rocky-linux', 'ubuntu-22.04', 'ubuntu-23.04'): - error( - f'distro {colors.bold}{distro}{colors.reset} not supported') if subprocess.run(['podman', 'container', 'exists', container_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0: error(f'container {colors.bold}{container_name}{colors.reset} already exists') exit(1) - subprocess.run(['blend', 'create-container', '-cn', container_name, '-d', distro]) + args = ['blend', 'create-container', '-cn', container_name] + # blend handles no distro being specified already + if distro: + args.extend(['-d', distro]) + exit(subprocess.run(args).returncode) @cli.command("delete-container") From e4d2ca043ca2d76c801eb0e132b9d6125648fcc9 Mon Sep 17 00:00:00 2001 From: askiiart Date: Tue, 8 Apr 2025 21:18:14 -0500 Subject: [PATCH 113/121] bit of code cleanup, format --- blend | 10 +++++----- user | 23 +++++++++++++++-------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/blend b/blend index d34eda5..c10175d 100755 --- a/blend +++ b/blend @@ -129,7 +129,7 @@ def check_container(name): def check_container_status(name): - if os.environ.get('SUDO_USER') == None: + if os.environ.get('SUDO_USER'): return host_get_output("podman inspect --type container " + name + " --format \"{{.State.Status}}\"") else: return host_get_output(f"sudo -u {os.environ.get('SUDO_USER')} podman inspect --type container " + name + " --format \"{{.State.Status}}\"") @@ -137,7 +137,7 @@ def check_container_status(name): def core_start_container(name, new_container=False): sudo = [] - if os.environ.get('SUDO_USER') != None: + if os.environ.get('SUDO_USER'): sudo = ['sudo', '-u', os.environ.get('SUDO_USER')] subprocess.call([*sudo, 'podman', 'start', name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) @@ -150,7 +150,7 @@ def core_start_container(name, new_container=False): subprocess.call(['podman', 'logs', '--since', str(start_time), name]) exit(1) - if os.environ.get('SUDO_USER') == None: + if not os.environ.get('SUDO_USER'): logproc = pexpect.spawn( 'podman', args=['logs', '-f', '--since', str(start_time), name], timeout=3600) else: @@ -411,7 +411,7 @@ def enter_container(): podman_args = ['--env', 'LC_ALL=C.UTF-8'] sudo = [] - if os.environ.get('SUDO_USER') == None: + if not os.environ.get('SUDO_USER'): podman_args = ['--user', getpass.getuser()] else: sudo = ['sudo', '-u', os.environ.get( @@ -420,7 +420,7 @@ def enter_container(): if name not in ['LANG', 'LC_CTYPE', 'LC_ALL', 'PATH', 'HOST', 'HOSTNAME', 'SHELL'] and not name.startswith('_'): podman_args.append('--env') podman_args.append(name + '=' + val) - if os.environ.get('BLEND_COMMAND') == None or os.environ.get('BLEND_COMMAND') == '': + if not os.environ.get('BLEND_COMMAND'): if args.pkg == []: if os.getcwd() == os.path.expanduser('~') or os.getcwd().startswith(os.path.expanduser('~') + '/'): exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, diff --git a/user b/user index 98b93cd..a9b9b03 100755 --- a/user +++ b/user @@ -162,7 +162,8 @@ def create_container(container_name, distro): Create a container ''' if subprocess.run(['podman', 'container', 'exists', container_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0: - error(f'container {colors.bold}{container_name}{colors.reset} already exists') + error( + f'container {colors.bold}{container_name}{colors.reset} already exists') exit(1) args = ['blend', 'create-container', '-cn', container_name] # blend handles no distro being specified already @@ -178,7 +179,8 @@ def delete_container(container): Delete a container ''' if subprocess.run(['podman', 'container', 'exists', container], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0: - error(f'container {colors.bold}{container}{colors.reset} does not exist') + error( + f'container {colors.bold}{container}{colors.reset} does not exist') exit(1) subprocess.run(['blend', 'remove-container', container]) @@ -190,7 +192,8 @@ def shell(container): Enter a shell inside a container ''' if subprocess.run(['podman', 'container', 'exists', container], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0: - error(f'container {colors.bold}{container}{colors.reset} does not exist') + error( + f'container {colors.bold}{container}{colors.reset} does not exist') exit(1) creation_env = os.environ.copy() creation_env['BLEND_NO_CHECK'] = 'true' @@ -205,12 +208,14 @@ def exec_c(container, cmds): Run a command inside a container ''' if subprocess.run(['podman', 'container', 'exists', container], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0: - error(f'container {colors.bold}{container}{colors.reset} does not exist') + error( + f'container {colors.bold}{container}{colors.reset} does not exist') exit(1) creation_env = os.environ.copy() creation_env['BLEND_NO_CHECK'] = 'true' - cmds = [ cmd.replace('\\-', '-') for cmd in cmds] - subprocess.run(['blend', 'enter', '-cn', container, '--', *cmds], env=creation_env) + cmds = [cmd.replace('\\-', '-') for cmd in cmds] + subprocess.run(['blend', 'enter', '-cn', container, + '--', *cmds], env=creation_env) @cli.command("install") @@ -228,7 +233,8 @@ def install_c(container, pkgs): elif os.path.isfile(os.path.expanduser(f'~/.local/bin/blend_bin/pacman.{container}')): subprocess.run([f'sudo.{container}', 'pacman', '-Syu', *pkgs]) else: - error(f'container {colors.bold}{container}{colors.reset} does not exist') + error( + f'container {colors.bold}{container}{colors.reset} does not exist') exit(1) @@ -246,7 +252,8 @@ def remove_c(container, pkgs): elif os.path.isfile(os.path.expanduser(f'~/.local/bin/blend_bin/pacman.{container}')): subprocess.run([f'sudo.{container}', 'pacman', '-Rcns', *pkgs]) else: - error(f'container {colors.bold}{container}{colors.reset} does not exist') + error( + f'container {colors.bold}{container}{colors.reset} does not exist') exit(1) From fce7c69dc749d61a044d75fedbedc56d3c0e3cc9 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Wed, 23 Apr 2025 15:44:57 +0100 Subject: [PATCH 114/121] chore: refactor code --- blend | 3 +- blend-settings/main.js | 21 +- blend-settings/src/index.html | 10 +- blend-system | 153 --------- blend.hook | 56 ---- blend.install | 19 -- overlayfs-tools/README.md | 42 --- overlayfs-tools/logic.c | 581 ---------------------------------- overlayfs-tools/logic.h | 37 --- overlayfs-tools/main.c | 266 ---------------- overlayfs-tools/makefile | 23 -- overlayfs-tools/sh.c | 98 ------ overlayfs-tools/sh.h | 20 -- 13 files changed, 23 insertions(+), 1306 deletions(-) delete mode 100755 blend-system delete mode 100755 blend.hook delete mode 100644 blend.install delete mode 100755 overlayfs-tools/README.md delete mode 100755 overlayfs-tools/logic.c delete mode 100755 overlayfs-tools/logic.h delete mode 100755 overlayfs-tools/main.c delete mode 100755 overlayfs-tools/makefile delete mode 100755 overlayfs-tools/sh.c delete mode 100755 overlayfs-tools/sh.h diff --git a/blend b/blend index c10175d..014989c 100755 --- a/blend +++ b/blend @@ -129,7 +129,7 @@ def check_container(name): def check_container_status(name): - if os.environ.get('SUDO_USER'): + if not os.environ.get('SUDO_USER'): return host_get_output("podman inspect --type container " + name + " --format \"{{.State.Status}}\"") else: return host_get_output(f"sudo -u {os.environ.get('SUDO_USER')} podman inspect --type container " + name + " --format \"{{.State.Status}}\"") @@ -143,6 +143,7 @@ def core_start_container(name, new_container=False): stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) start_time = time.time() - 1000 # workaround + time.sleep(1) if check_container_status(name) != 'running': print('') error('the entry point failed to run; try again later') diff --git a/blend-settings/main.js b/blend-settings/main.js index e4eac38..aeda78f 100644 --- a/blend-settings/main.js +++ b/blend-settings/main.js @@ -160,10 +160,15 @@ function loadTerminalWindow(title, cmd) { if (!terminalWindow.isDestroyed()) { terminalWindow.webContents.send("terminal.reset") terminalWindow.hide() - if (title.startsWith('Creating container: ')) { - mainWindow.webContents.send("container-created") - } else if (title.startsWith('Package installation')) { - packageWindow.webContents.send("installation-complete") + try { + if (title.startsWith('Creating container: ')) { + mainWindow.webContents.send("container-created") + } else if (title.startsWith('Package installation')) { + packageWindow.webContents.send("installation-complete") + } + } catch (err) { + console.log(err) + app.quit() } } }) @@ -178,10 +183,8 @@ function loadTerminalWindow(title, cmd) { app.whenReady().then(() => { app.allowRendererProcessReuse = false - if (process.argv.length > 2) { - if (process.argv[2] == 'package') { - createPackageWindow() - } + if (process.argv.includes('package')) { + createPackageWindow() } else { createWindow() } @@ -196,4 +199,4 @@ app.whenReady().then(() => { app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit() -}) \ No newline at end of file +}) diff --git a/blend-settings/src/index.html b/blend-settings/src/index.html index d66f49c..1ef4cc7 100644 --- a/blend-settings/src/index.html +++ b/blend-settings/src/index.html @@ -18,7 +18,7 @@ Containers - +
@@ -43,6 +43,14 @@ if (fs.existsSync('/usr/bin/waydroid')) { document.getElementById('android-button').classList.remove('d-none') + } else { + document.getElementById('android-button').remove() + } + + if (fs.existsSync('/usr/bin/akshara')) { + document.getElementById('system-button').classList.remove('d-none') + } else { + document.getElementById('system-button').remove() } function page(page) { diff --git a/blend-system b/blend-system deleted file mode 100755 index 4fbf012..0000000 --- a/blend-system +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/python3 -# Copyright (C) 2023 Rudra Saraswat -# -# This file is part of blend. -# -# blend is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# blend is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with blend. If not, see . - - -import os, re, sys, time -import argparse -import subprocess - -__version = '2.0.0' - -### Colors -class colors: - reset = '\033[0m' - bold = '\033[01m' - disable = '\033[02m' - underline = '\033[04m' - reverse = '\033[07m' - strikethrough = '\033[09m' - invisible = '\033[08m' - - class fg: - black = '\033[30m' - red = '\033[31m' - green = '\033[32m' - orange = '\033[33m' - blue = '\033[34m' - purple = '\033[35m' - cyan = '\033[36m' - lightgrey = '\033[37m' - darkgrey = '\033[90m' - lightred = '\033[91m' - lightgreen = '\033[92m' - yellow = '\033[93m' - lightblue = '\033[94m' - pink = '\033[95m' - lightcyan = '\033[96m' - - class bg: - black = '\033[40m' - red = '\033[41m' - green = '\033[42m' - orange = '\033[43m' - blue = '\033[44m' - purple = '\033[45m' - cyan = '\033[46m' - lightgrey = '\033[47m' - -### END - -### Helper functions - -def info(msg): - print (colors.bold + colors.fg.cyan + '>> i: ' + colors.reset + colors.bold + msg + colors.reset) - -def error(err): - print (colors.bold + colors.fg.red + '>> e: ' + colors.reset + colors.bold + err + colors.reset) - -### END - -def current_state(): - _state = -1 - for s in os.listdir('/.states'): - if re.match(r'^state([0-9]+)\.squashfs$', s): - if int(s[5:-7]) > _state: - _state = int(s[5:-7]) - return _state - -def save_state(): - subprocess.call(['mkdir', '-p', '/.states'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - state = current_state() + 1 - - subprocess.call(['bash', '-c', 'rm -f /.states/*.tmp']) - - if subprocess.call(['mksquashfs', '/usr', f'/.states/state{state}.squashfs.tmp', '-no-compression'], stdout=sys.stdout, stderr=sys.stderr) == 0: - subprocess.call(['rm', '-rf', 'add-squashfs'], cwd='/tmp') - subprocess.call(['mkdir', '-p', 'add-squashfs'], cwd='/tmp') - subprocess.call(['cp', '-a', '/var/lib', 'add-squashfs/varlib'], cwd='/tmp') - if subprocess.call(['mksquashfs', 'add-squashfs', f'/.states/state{state}.squashfs.tmp', '-no-compression'], cwd='/tmp') == 0: - subprocess.call(['mv', f'/.states/state{state}.squashfs.tmp', f'/.states/state{state}.squashfs']) - else: - error('state creation failed') - exit(1) - else: - error('state creation failed') - exit(1) - - info(f'saved state {state}') - -def rollback(): - info("Rollback hasn't been implemented yet.") - -description = f''' -{colors.bold}{colors.fg.purple}Usage:{colors.reset} - blend-system [command] [options] [arguments] - -{colors.bold}{colors.fg.purple}Version:{colors.reset} {__version}{colors.bold} - -{colors.bold}{colors.fg.purple}available commands{colors.reset}: - {colors.bold}help{colors.reset} Show this help message and exit. - {colors.bold}version{colors.reset} Show version information and exit. - {colors.bold}save-state{colors.reset} Save the current state (backup). - {colors.bold}rollback{colors.reset} Rollback to previous state. - -{colors.bold}{colors.fg.purple}options for commands{colors.reset}: - {colors.bold}-v, --version{colors.reset} show version information and exit -''' - -epilog = f''' -{colors.bold}Made with {colors.fg.red}\u2764{colors.reset}{colors.bold} by Rudra Saraswat.{colors.reset} -''' - -parser = argparse.ArgumentParser(description=description, usage=argparse.SUPPRESS, - epilog=epilog, formatter_class=argparse.RawTextHelpFormatter) -command_map = { 'help': 'help', - 'version': 'version', - 'save-state': save_state, - 'rollback': rollback } -parser.add_argument('command', choices=command_map.keys(), help=argparse.SUPPRESS) -parser.add_argument('-v', '--version', action='version', version=f'%(prog)s {__version}', help=argparse.SUPPRESS) - -if len(sys.argv) == 1: - parser.print_help() - exit() - -if os.geteuid() != 0: - error('requires root') - exit(1) - -args = parser.parse_intermixed_args() - -command = command_map[args.command] - -if command == 'help': - parser.print_help() -elif command == 'version': - parser.parse_args(['--version']) -else: - command() diff --git a/blend.hook b/blend.hook deleted file mode 100755 index 0eb8f44..0000000 --- a/blend.hook +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash - -run_latehook() { - echo - - if [[ "$abort_staging" == true ]]; then - echo '[ BLEND ] Not applying system changes made in previous boot.' - rm -rf '/new_root/.upperdir' '/new_root/.workdir'; mkdir -p '/new_root/.upperdir' '/new_root/.workdir' - fi - - if [[ -d "/new_root/blend/overlay/current" ]]; then - echo '[ BLEND ] Detected old version of overlay implementation, switching.' - rm -rf /new_root/.upperdir /new_root/.workdir - mv /new_root/blend/overlay/current/usr /new_root/.upperdir - rm -rf /new_root/blend - fi - - # Broken attempt at getting rollback and snapshots working. - # - # if [[ -L "/new_root/.states/rollback.squashfs" ]] && [[ -e "/new_root/.states/rollback.squashfs" ]]; then - # echo -n '[ BLEND ] Rolling back to selected state. Do __not__ power off or reboot.' - # echo - # cd /new_root - # unsquashfs /new_root/.states/rollback.squashfs && ( - # for i in bin include lib lib32 share src; do - # rm -rf rm -rf /new_root/.workdir/"$i" rm -rf /new_root/.upperdir/"$i" /new_root/usr/"$i" - # mv squashfs-root/"$i" /new_root/usr - # done - # rm -rf /new_root/.workdir/varlib /new_root/.upperdir/varlib /new_root/var/lib - # mkdir -p /new_root/var/lib - # mv squashfs-root/varlib /new_root/var/varlib - # echo ' - SUCCESS ' - # echo - # ); cd .. - # fi - - for i in bin include lib lib32 share src; do - echo -n "[ BLEND ] Setting up /usr/${i} overlay (applying changes)." - rm -rf /new_root/.workdir/"$i" - mkdir -p /new_root/.upperdir/"$i" /new_root/.workdir/"$i" /new_root/usr/"$i" /new_root/tmp - cd /new_root/tmp; overlayfs-tools merge -l /new_root/usr/$i -u /new_root/.upperdir/$i &>/dev/null; chmod 755 ./overlay-tools-*; ./overlay-tools-* &>/dev/null; rm -f ./overlay-tools-*; cd / - mkdir -p /new_root/.upperdir/"$i" - mount -t overlay overlay -o 'lowerdir=/new_root/usr/'$i',upperdir=/new_root/.upperdir/'$i',workdir=/new_root/.workdir/'$i /new_root/usr/"$i" -o index=off - echo " - SUCCESS" - done - - echo - echo -n "[ BLEND ] Setting up /var/lib overlay (applying changes)." - rm -rf /new_root/.workdir/varlib - mkdir -p /new_root/.upperdir/varlib /new_root/.workdir/varlib /new_root/var/lib /new_root/tmp - cd /new_root/tmp; overlayfs-tools merge -l /new_root/var/lib -u /new_root/.upperdir/varlib &>/dev/null; chmod 755 ./overlay-tools-*; ./overlay-tools-* &>/dev/null; rm -f ./overlay-tools-*; cd / - mkdir -p /new_root/.upperdir/varlib - mount -t overlay overlay -o 'lowerdir=/new_root/var/lib,upperdir=/new_root/.upperdir/varlib,workdir=/new_root/.workdir/varlib' /new_root/var/lib -o index=off - echo ' - SUCCESS' - echo -} diff --git a/blend.install b/blend.install deleted file mode 100644 index 434a035..0000000 --- a/blend.install +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: GPL-3.0 - -build() { - add_module overlay - add_binary bash - add_binary tar - add_binary overlayfs-tools - add_runscript -} - -help() { - cat < A directory is made opaque by setting the xattr "trusted.overlay.opaque" to "y". - -However, only users with `CAP_SYS_ADMIN` can read `trusted.*` extended attributes. - -Warnings / limitations --------- - -- Only works for regular files and directories. Do not use it on OverlayFS with device files, socket files, etc.. -- Hard links may be broken (i.e. resulting in duplicated independent files). -- File owner, group and permission bits will be preserved. File timestamps, attributes and extended attributes might be lost. -- This program only works for OverlayFS with only one lower layer. -- It is recommended to have the OverlayFS unmounted before running this program. diff --git a/overlayfs-tools/logic.c b/overlayfs-tools/logic.c deleted file mode 100755 index 47ebfaa..0000000 --- a/overlayfs-tools/logic.c +++ /dev/null @@ -1,581 +0,0 @@ -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "logic.h" -#include "sh.h" - -// exactly the same as in linux/fs.h -#define WHITEOUT_DEV 0 - -// exact the same as in fs/overlayfs/overlayfs.h -const char *ovl_opaque_xattr = "trusted.overlay.opaque"; -const char *ovl_redirect_xattr = "trusted.overlay.redirect"; -const char *ovl_metacopy_xattr = "trusted.overlay.metacopy"; - -#define MIN(X,Y) ((X) < (Y) ? (X) : (Y)) - -#define TRAILING_SLASH(ftype) (((ftype) == S_IFDIR) ? "/" : "") - -static inline mode_t file_type(const struct stat *status) { - return status->st_mode & S_IFMT; -} - -const char *ftype_name(mode_t type) { - switch (type) { - case S_IFDIR: - return "directory"; - case S_IFREG: - return "regular file"; - case S_IFLNK: - return "symbolic link"; - default: - return "special file"; - } -} - -const char *ftype_name_plural(mode_t type) { - switch (type) { - case S_IFDIR: - return "Directories"; - case S_IFREG: - return "Files"; - case S_IFLNK: - return "Symbolic links"; - default: - return "Special files"; - } -} - -static inline bool is_whiteout(const struct stat *status) { - return (file_type(status) == S_IFCHR) && (status->st_rdev == WHITEOUT_DEV); -} - -static inline mode_t permission_bits(const struct stat *status) { // not used yet. I haven't decided how to treat permission bit changes - return status->st_mode & (S_IRWXU | S_IRWXG | S_IRWXO | S_ISVTX); -} - -int is_opaque(const char *path, bool *output) { - char val; - ssize_t res = getxattr(path, ovl_opaque_xattr, &val, 1); - if ((res < 0) && (errno != ENODATA)) { - return -1; - } - *output = (res == 1 && val == 'y'); - return 0; -} - -int is_redirect(const char *path, bool *output) { - ssize_t res = getxattr(path, ovl_redirect_xattr, NULL, 0); - if ((res < 0) && (errno != ENODATA)) { - fprintf(stderr, "File %s redirect xattr can not be read.\n", path); - return -1; - } - *output = (res > 0); - return 0; -} - -int is_metacopy(const char *path, bool *output) { - ssize_t res = getxattr(path, ovl_metacopy_xattr, NULL, 0); - if ((res < 0) && (errno != ENODATA)) { - fprintf(stderr, "File %s metacopy xattr can not be read.\n", path); - return -1; - } - *output = (res >= 0); - return 0; -} - -// Treat redirect as opaque dir because it hides the tree in lower_path -// and we do not support following to redirected lower path -int is_opaquedir(const char *path, bool *output) { - bool opaque, redirect; - if (is_opaque(path, &opaque) < 0) { return -1; } - if (is_redirect(path, &redirect) < 0) { return -1; } - *output = opaque || redirect; - return 0; -} - -bool permission_identical(const struct stat *lower_status, const struct stat *upper_status) { - return (permission_bits(lower_status) == permission_bits(upper_status)) && (lower_status->st_uid == upper_status->st_uid) && (lower_status->st_gid == upper_status->st_gid); -} - -int read_chunk(int fd, char *buf, int len) { - ssize_t ret; - ssize_t remain = len; - while (remain > 0 && (ret = read(fd, buf, remain)) != 0) { - if (ret == -1) { - if (errno == EINTR) { - continue; - } - return -1; - } - remain -= ret; - buf += ret; - } - return len - remain; -} - -int regular_file_identical(const char *lower_path, const struct stat *lower_status, const char *upper_path, const struct stat *upper_status, bool *output) { - size_t blksize = (size_t) MIN(lower_status->st_blksize, upper_status->st_blksize); - if (lower_status->st_size != upper_status->st_size) { // different sizes - *output = false; - return 0; - } - bool metacopy, redirect; - if (is_metacopy(upper_path, &metacopy) < 0) { return -1; } - if (is_redirect(upper_path, &redirect) < 0) { return -1; } - if (metacopy) { - // metacopy means data is indentical, but redirect means it is not identical to lower_path - *output = !redirect; - return 0; - } - char lower_buffer[blksize]; - char upper_buffer[blksize]; - int lower_file = open(lower_path, O_RDONLY); - int upper_file = open(upper_path, O_RDONLY); - if (lower_file < 0) { - fprintf(stderr, "File %s can not be read for content.\n", lower_path); - return -1; - } - if (upper_file < 0) { - fprintf(stderr, "File %s can not be read for content.\n", upper_path); - return -1; - } - ssize_t read_lower; ssize_t read_upper; - do { // we can assume one will not reach EOF earlier than the other, as the file sizes are checked to be the same earlier - read_lower = read_chunk(lower_file, lower_buffer, blksize); - read_upper = read_chunk(upper_file, upper_buffer, blksize); - if (read_lower < 0) { - fprintf(stderr, "Error occured when reading file %s.\n", lower_path); - return -1; - } - if (read_upper < 0) { - fprintf(stderr, "Error occured when reading file %s.\n", upper_path); - return -1; - } - if (read_upper != read_lower) { // this should not happen as we've checked the sizes - fprintf(stderr, "Unexpected size difference: %s.\n", upper_path); - return -1; - } - if (memcmp(lower_buffer, upper_buffer, read_upper)) { *output = false; return 0; } // the output is by default false, but we still set it for ease of reading - } while (read_lower || read_upper); - *output = true; // now we can say they are identical - if (close(lower_file) || close(upper_file)) { return -1; } - return 0; -} - -int symbolic_link_identical(const char *lower_path, const char *upper_path, bool *output) { - char lower_buffer[PATH_MAX]; - char upper_buffer[PATH_MAX]; - ssize_t lower_len = readlink(lower_path, lower_buffer, PATH_MAX); - ssize_t upper_len = readlink(upper_path, upper_buffer, PATH_MAX); - if (lower_len < 0 || lower_len == PATH_MAX) { - fprintf(stderr, "Symbolic link %s cannot be resolved.\n", lower_path); - return -1; - } - if (upper_len < 0 || upper_len == PATH_MAX) { - fprintf(stderr, "Symbolic link %s cannot be resolved.\n", upper_path); - return -1; - } - lower_buffer[lower_len] = '\0'; - upper_buffer[upper_len] = '\0'; - *output = (strcmp(lower_buffer, upper_buffer) == 0); - return 0; -} - -static int vacuum_d(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { - bool opaque; - if (is_opaquedir(upper_path, &opaque) < 0) { return -1; } - if (opaque) { // TODO: sometimes removing opaque directory (and combine with lower directory) might be better - *fts_instr = FTS_SKIP; - } - return 0; -} - -static int vacuum_dp(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { - if (lower_status == NULL) { return 0; } // lower does not exist - if (file_type(lower_status) != S_IFDIR) { return 0; } - if (!permission_identical(lower_status, upper_status)) { return 0; } - bool opaque; - if (is_opaquedir(upper_path, &opaque) < 0) { - return -1; - } - if (opaque) { return 0; } - // this directory might be empty if all children are deleted in previous commands. but we simply don't test whether it's that case - return command(script_stream, "rmdir --ignore-fail-on-non-empty %U", upper_path); -} - -static int vacuum_f(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { - if (lower_status == NULL) { return 0; } // lower does not exist - if (file_type(lower_status) != S_IFREG) { return 0; } - if (!permission_identical(lower_status, upper_status)) { return 0; } - bool identical; - if (regular_file_identical(lower_path, lower_status, upper_path, upper_status, &identical) < 0) { - return -1; - } - if (!identical) { return 0; } - return command(script_stream, "rm %U", upper_path); -} - -static int vacuum_sl(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { - if (lower_status == NULL) { return 0; } // lower does not exist - if (file_type(lower_status) != S_IFLNK) { return 0; } - if (!permission_identical(lower_status, upper_status)) { return 0; } - bool identical; - if (symbolic_link_identical(lower_path, upper_path, &identical) < 0) { - return -1; - } - if (!identical) { return 0; } - return command(script_stream, "rm %U", upper_path); -} - -void print_only_in(const char *path) { - char *dirc = strdup(path); - char *basec = strdup(path); - char *dname = dirname(dirc); - char *bname = basename(basec); - printf("Only in %s: %s\n", dname, bname); - free(dirc); - free(basec); -} - -void print_removed(const char *lower_path, const size_t lower_root_len, mode_t lower_type) { - if (brief) { - print_only_in(lower_path); - } else { - printf("Removed: %s%s\n", &lower_path[lower_root_len], TRAILING_SLASH(lower_type)); - } -} - -void print_added(const char *lower_path, const size_t lower_root_len, const char *upper_path, mode_t upper_type) { - if (brief) { - print_only_in(upper_path); - } else { - printf("Added: %s%s\n", &lower_path[lower_root_len], TRAILING_SLASH(upper_type)); - } -} - -void print_replaced(const char *lower_path, const size_t lower_root_len, mode_t lower_type, const char *upper_path, mode_t upper_type) { - if (brief) { - printf("File %s is a %s while file %s is a %s\n", lower_path, ftype_name(lower_type), upper_path, ftype_name(upper_type)); - } else { - if (lower_type != S_IFDIR) { // dir removed already printed by list_deleted_files() - print_removed(lower_path, lower_root_len, lower_type); - } - print_added(lower_path, lower_root_len, upper_path, upper_type); - } -} - -void print_modified(const char *lower_path, const size_t lower_root_len, mode_t lower_type, const char *upper_path, bool identical) { - if (brief) { - if (!identical) { // brief format does not print permission difference - printf("%s %s and %s differ\n", ftype_name_plural(lower_type), lower_path, upper_path); - } - } else { - printf("Modified: %s%s\n", &lower_path[lower_root_len], TRAILING_SLASH(lower_type)); - } -} - -int list_deleted_files(const char *lower_path, size_t lower_root_len, mode_t upper_type) { // This WORKS with files and itself is listed. However, prefixs are WRONG! - // brief format needs to print only first level deleted children under opaque dir - bool children = (brief && (upper_type == S_IFDIR)); - if (!verbose && !children) { - if (!brief || upper_type == S_IFCHR) { // dir replaced already printed by print_replaced() - print_removed(lower_path, lower_root_len, S_IFDIR); - } - return 0; - } - FTSENT *cur; - char *paths[2] = {(char *) lower_path, NULL }; - FTS *ftsp = fts_open(paths, FTS_NOCHDIR | FTS_PHYSICAL, NULL); - if (ftsp == NULL) { return -1; } - int return_val = 0; - while (((cur = fts_read(ftsp)) != NULL) && (return_val == 0)) { - switch (cur->fts_info) { - case FTS_D: - // brief format does not need to print deleted grand children under opaque dir - if (children && cur->fts_level > 0) { - print_removed(cur->fts_path, lower_root_len, S_IFDIR); - fts_set(ftsp, cur, FTS_SKIP); - } - break; // do nothing - case FTS_DP: - // brief format does not need to print deleted dir under opaque dir itself - if (!children) { - print_removed(cur->fts_path, lower_root_len, S_IFDIR); - } - break; - case FTS_F: - print_removed(cur->fts_path, lower_root_len, S_IFREG); - break; - case FTS_SL: - print_removed(cur->fts_path, lower_root_len, S_IFLNK); - break; - case FTS_DEFAULT: - fprintf(stderr, "File %s is a special file (device or pipe). We cannot handle that.\n", cur->fts_path); - return_val = -1; - break; - default: - fprintf(stderr, "Error occured when opening %s.\n", cur->fts_path); - return_val = -1; - } - } - if (errno) { return_val = -1; } // if no error happened, fts_read will "sets the external variable errno to 0" according to the documentation - return fts_close(ftsp) || return_val; -} - -static int diff_d(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { - bool opaque = false; - bool lower_exist = (lower_status != NULL); - if (lower_exist) { - if (file_type(lower_status) == S_IFDIR) { - if (is_opaquedir(upper_path, &opaque) < 0) { return -1; } - if (opaque) { - if (list_deleted_files(lower_path, lower_root_len, S_IFDIR) < 0) { return -1; } - } else { - if (!permission_identical(lower_status, upper_status)) { - print_modified(lower_path, lower_root_len, S_IFDIR, upper_path, true); - } - return 0; // children must be recursed, and directory itself does not need to be printed - } - } else { // other types of files - print_replaced(lower_path, lower_root_len, file_type(lower_status), upper_path, S_IFDIR); - } - } - if (!(verbose || (brief && opaque))) { // brief format needs to print children of opaque dir - *fts_instr = FTS_SKIP; - } - if (!lower_exist || (!brief && opaque)) { // brief format does not need to print opaque dir itself - print_added(lower_path, lower_root_len, upper_path, S_IFDIR); - } - return 0; -} - -static int diff_f(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { - bool identical; - if (lower_status != NULL) { - switch (file_type(lower_status)) { - case S_IFREG: - if (regular_file_identical(lower_path, lower_status, upper_path, upper_status, &identical) < 0) { - return -1; - } - if (!(identical && permission_identical(lower_status, upper_status))) { - print_modified(lower_path, lower_root_len, S_IFREG, upper_path, identical); - } - return 0; - case S_IFDIR: - if (list_deleted_files(lower_path, lower_root_len, S_IFREG) < 0) { return -1; } - /* fallthrough */ - case S_IFLNK: - print_replaced(lower_path, lower_root_len, file_type(lower_status), upper_path, S_IFREG); - return 0; - default: - fprintf(stderr, "File %s is a special file (device or pipe). We cannot handle that.\n", lower_path); - return -1; - } - } - print_added(lower_path, lower_root_len, upper_path, S_IFREG); - return 0; -} - -static int diff_sl(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { - bool identical; - if (lower_status != NULL) { - switch (file_type(lower_status)) { - case S_IFDIR: - if (list_deleted_files(lower_path, lower_root_len, S_IFLNK) < 0) { return -1; } - /* fallthrough */ - case S_IFREG: - print_replaced(lower_path, lower_root_len, file_type(lower_status), upper_path, S_IFLNK); - return 0; - case S_IFLNK: - if (symbolic_link_identical(lower_path, upper_path, &identical) < 0) { - return -1; - } - if (!(identical && permission_identical(lower_status, upper_status))) { - print_modified(lower_path, lower_root_len, S_IFLNK, upper_path, identical); - } - return 0; - default: - fprintf(stderr, "File %s is a special file (device or pipe). We cannot handle that.\n", lower_path); - return -1; - } - } - print_added(lower_path, lower_root_len, upper_path, S_IFLNK); - return 0; -} - -static int diff_whiteout(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { - if (lower_status != NULL) { - if (file_type(lower_status) == S_IFDIR) { - if (list_deleted_files(lower_path, lower_root_len, S_IFCHR) < 0) { return -1; } - } else { - print_removed(lower_path, lower_root_len, file_type(lower_status)); - } - } // else: whiteouting a nonexistent file? must be an error. but we ignore that :) - return 0; -} - -static int merge_d(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { - bool redirect; - if (is_redirect(upper_path, &redirect) < 0) { return -1; } - // merging redirects is not supported, we must abort merge so redirected lower (under whiteout) won't be deleted - // upper_path may be hiding the directory in lower_path, but there may be another redirect upper pointing at it - if (redirect) { - fprintf(stderr, "Found redirect on %s. Merging redirect is not supported - Abort.\n", upper_path); - return -1; - } - if (lower_status != NULL) { - if (file_type(lower_status) == S_IFDIR) { - bool opaque = false; - if (is_opaquedir(upper_path, &opaque) < 0) { return -1; } - if (opaque) { - if (command(script_stream, "rm -r %L", lower_path) < 0) { return -1; }; - } else { - if (!permission_identical(lower_status, upper_status)) { - command(script_stream, "chmod --reference %U %L", upper_path, lower_path); - } - return 0; // children must be recursed, and directory itself does not need to be printed - } - } else { - command(script_stream, "rm %L", lower_path); - } - } - *fts_instr = FTS_SKIP; - return command(script_stream, "mv -T %U %L", upper_path, lower_path); -} - -static int merge_dp(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { - if (lower_status != NULL) { - if (file_type(lower_status) == S_IFDIR) { - bool opaque = false; - if (is_opaquedir(upper_path, &opaque) < 0) { return -1; } - if (!opaque) { // delete the directory: it should be empty already - return command(script_stream, "rmdir %U", upper_path); - } - } - } - return 0; -} - -static int merge_f(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { - bool metacopy, redirect; - if (is_metacopy(upper_path, &metacopy) < 0) { return -1; } - if (is_redirect(upper_path, &redirect) < 0) { return -1; } - // merging metacopy is not supported, we must abort merge so lower data won't be deleted - if (metacopy || redirect) { - fprintf(stderr, "Found metacopy/redirect on %s. Merging metacopy/redirect is not supported - Abort.\n", upper_path); - return -1; - } - return command(script_stream, "rm -rf %L", lower_path) || command(script_stream, "mv -T %U %L", upper_path, lower_path); -} - -static int merge_sl(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { - return command(script_stream, "rm -rf %L", lower_path) || command(script_stream, "mv -T %U %L", upper_path, lower_path); -} - -static int merge_whiteout(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { - return command(script_stream, "rm -r %L", lower_path) || command(script_stream, "rm %U", upper_path); -} - -typedef int (*TRAVERSE_CALLBACK)(const char *lower_path, const char* upper_path, const size_t lower_root_len, const struct stat *lower_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr); - -int traverse(const char *lower_root, const char *upper_root, FILE* script_stream, TRAVERSE_CALLBACK callback_d, TRAVERSE_CALLBACK callback_dp, TRAVERSE_CALLBACK callback_f, TRAVERSE_CALLBACK callback_sl, TRAVERSE_CALLBACK callback_whiteout) { // returns 0 on success - FTSENT *cur; - char *paths[2] = {(char *) upper_root, NULL }; - char lower_path[PATH_MAX]; - strcpy(lower_path, lower_root); - size_t upper_root_len = strlen(upper_root); - size_t lower_root_len = strlen(lower_root); - FTS *ftsp = fts_open(paths, FTS_NOCHDIR | FTS_PHYSICAL, NULL); - if (ftsp == NULL) { return -1; } - int return_val = 0; - while ((return_val == 0) && ((cur = fts_read(ftsp)) != NULL)) { - TRAVERSE_CALLBACK callback = NULL; - switch (cur->fts_info) { - case FTS_D: - callback = callback_d; - break; - case FTS_DP: - callback = callback_dp; - break; - case FTS_F: - callback = callback_f; - break; - case FTS_SL: - callback = callback_sl; - break; - case FTS_DEFAULT: - if (is_whiteout(cur->fts_statp)) { - callback = callback_whiteout; - } else { - return_val = -1; - fprintf(stderr, "File %s is a special file (device or pipe). We cannot handle that.\n", cur->fts_path); - } - break; - default: - return_val = -1; - fprintf(stderr, "Error occured when opening %s.\n", cur->fts_path); - } - if (callback != NULL) { - int fts_instr = 0; - struct stat lower_status; - bool lower_exist = true; - strcpy(&lower_path[lower_root_len], &(cur->fts_path[upper_root_len])); - if (lstat(lower_path, &lower_status) != 0) { - if (errno == ENOENT || errno == ENOTDIR) { // the corresponding lower file (or its ancestor) does not exist at all - lower_exist = false; - } else { // stat failed for some unknown reason - fprintf(stderr, "Failed to stat %s.\n", lower_path); - return_val = -1; - break; // do not call callback in this case - } - } - return_val = callback(lower_path, cur->fts_path, lower_root_len, lower_exist ? &lower_status : NULL, cur->fts_statp, script_stream, &fts_instr); // return_val must previously be 0 - if (fts_instr) { - fts_set(ftsp, cur, fts_instr); - } - } - } - if (errno) { return_val = -1; } // if no error happened, fts_read will "sets the external variable errno to 0" according to the documentation - return fts_close(ftsp) || return_val; -} - -static int deref_d(const char *mnt_path, const char* upper_path, const size_t mnt_root_len, const struct stat *mnt_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { - bool redirect; - if (is_redirect(upper_path, &redirect) < 0) { return -1; } - if (!redirect) { return 0; } - *fts_instr = FTS_SKIP; - return command(script_stream, "rm -rf %U", upper_path) || command(script_stream, "cp -a %M %U", mnt_path, upper_path); -} - -static int deref_f(const char *mnt_path, const char* upper_path, const size_t mnt_root_len, const struct stat *mnt_status, const struct stat *upper_status, FILE* script_stream, int *fts_instr) { - bool metacopy; - if (is_metacopy(upper_path, &metacopy) < 0) { return -1; } - if (!metacopy) { return 0; } - return command(script_stream, "rm -r %U", upper_path) || command(script_stream, "cp -a %M %U", mnt_path, upper_path); -} - -int vacuum(const char* lowerdir, const char* upperdir, FILE* script_stream) { - return traverse(lowerdir, upperdir, script_stream, vacuum_d, vacuum_dp, vacuum_f, vacuum_sl, NULL); -} - -int diff(const char* lowerdir, const char* upperdir) { - return traverse(lowerdir, upperdir, NULL, diff_d, NULL, diff_f, diff_sl, diff_whiteout); -} - -int merge(const char* lowerdir, const char* upperdir, FILE* script_stream) { - return traverse(lowerdir, upperdir, script_stream, merge_d, merge_dp, merge_f, merge_sl, merge_whiteout); -} - -int deref(const char* mountdir, const char* upperdir, FILE* script_stream) { - return traverse(mountdir, upperdir, script_stream, deref_d, NULL, deref_f, NULL, NULL); -} diff --git a/overlayfs-tools/logic.h b/overlayfs-tools/logic.h deleted file mode 100755 index 374e517..0000000 --- a/overlayfs-tools/logic.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - * logic.h / logic.c - * - * the logic for the three feature functions - */ - -#ifndef OVERLAYFS_TOOLS_LOGIC_H -#define OVERLAYFS_TOOLS_LOGIC_H - -#include - -extern bool verbose; -extern bool brief; - -/* - * feature function. will take very long time to complete. returns 0 on success - */ -int vacuum(const char* lowerdir, const char* upperdir, FILE* script_stream); - -/* - * feature function. will take very long time to complete. returns 0 on success - */ -int diff(const char* lowerdir, const char* upperdir); - -/* - * feature function. will take very long time to complete. returns 0 on success - */ -int merge(const char* lowerdir, const char* upperdir, FILE* script_stream); - -/* - * Unfold metacopy and redirect upper. - * - * mountdir is required and lowerdir is irrelevant. - */ -int deref(const char* mountdir, const char* upperdir, FILE* script_stream); - -#endif //OVERLAYFS_TOOLS_LOGIC_H diff --git a/overlayfs-tools/main.c b/overlayfs-tools/main.c deleted file mode 100755 index bea469f..0000000 --- a/overlayfs-tools/main.c +++ /dev/null @@ -1,266 +0,0 @@ -/* - * main.c - * - * the command line user interface - */ -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifndef _SYS_STAT_H - #include -#endif -#include "logic.h" -#include "sh.h" - -#define STRING_BUFFER_SIZE PATH_MAX * 2 - -// currently, brief and verbose are mutually exclusive -bool verbose; -bool brief; -bool yes; - -void print_help(const char *program) { - printf("Usage: %s command options\n", program); - puts(""); - puts("Commands:"); - puts(" vacuum - remove duplicated files in upperdir where copy_up is done but the file is not actually modified"); - puts(" diff - show the list of actually changed files"); - puts(" merge - merge all changes from upperdir to lowerdir, and clear upperdir"); - puts(" deref - copy changes from upperdir to a new upperdir unfolding redirect and metacopy"); - puts(""); - puts("Options:"); - puts(" -l, --lowerdir=LOWERDIR the lowerdir of OverlayFS (required)"); - puts(" -u, --upperdir=UPPERDIR the upperdir of OverlayFS (required)"); - puts(" -m, --mountdir=MOUNTDIR the mountdir of OverlayFS (optional)"); - puts(" -L, --lowernew=LOWERNEW the lowerdir of new OverlayFS (optional)"); - puts(" -U, --uppernew=UPPERNEW the upperdir of new OverlayFS (optional)"); - puts(" -y --yes don't prompt if OverlayFS is still mounted (optional)"); - puts(" -v, --verbose with diff action only: when a directory only exists in one version, still list every file of the directory"); - puts(" -b, --brief with diff action only: conform to output of diff --brief --recursive --no-dereference"); - puts(" -h, --help show this help text"); - puts(""); - puts("See https://github.com/kmxz/overlayfs-tools/ for warnings and more information."); -} - -bool starts_with(const char *haystack, const char* needle) { - return strncmp(needle, haystack, strlen(needle)) == 0; -} - -bool is_mounted(const char *lower, const char *upper) { - FILE *f = fopen("/proc/mounts", "r"); - if (!f) { - fprintf(stderr, "Cannot read /proc/mounts to test whether OverlayFS is mounted.\n"); - return true; - } - char buf[STRING_BUFFER_SIZE]; - while (fgets(buf, STRING_BUFFER_SIZE, f)) { - if (!starts_with(buf, "overlay")) { - continue; - } - if (strlen(buf) == STRING_BUFFER_SIZE) { - fprintf(stderr, "OverlayFS line in /proc/mounts is too long.\n"); - return true; - } - char *m_lower = strstr(buf, "lowerdir="); - char *m_upper = strstr(buf, "upperdir="); - if (m_lower == NULL || m_upper == NULL) { - fprintf(stderr, "Cannot extract information from OverlayFS line in /proc/mounts.\n"); - return true; - } - m_lower = &(m_lower[strlen("lowerdir=")]); - m_upper = &(m_upper[strlen("upperdir=")]); - if (!(strncmp(lower, m_lower, strlen(lower)) && strncmp(upper, m_upper, strlen(upper)))) { - printf("The OverlayFS involved is still mounted.\n"); - return true; - } - } - return false; -} - -bool check_mounted(const char *lower, const char *upper) { - if (is_mounted(lower, upper) && !yes) { - printf("It is strongly recommended to unmount OverlayFS first. Still continue (not recommended)?: \n"); - int r = getchar(); - if (r != 'Y' && r != 'y') { - return true; - } - } - return false; -} - -bool directory_exists(const char *path) { - struct stat sb; - if (lstat(path, &sb) != 0) { return false; } - return (sb.st_mode & S_IFMT) == S_IFDIR; -} - -bool directory_create(const char *name, const char *path) { - if (mkdir(path, 0755) == 0 || errno == EEXIST) { return true; } - fprintf(stderr, "%s directory '%s' does not exist and cannot be created.\n", name, path); - exit(EXIT_FAILURE); -} - -bool real_check_xattr_trusted(const char *tmp_path, int tmp_file) { - int ret = fsetxattr(tmp_file, "trusted.overlay.test", "naive", 5, 0); - close(tmp_file); - if (ret) { return false; } - char verify_buffer[10]; - if (getxattr(tmp_path, "trusted.overlay.test", verify_buffer, 10) != 5) { return false; } - return !strncmp(verify_buffer, "naive", 5); -} - -bool check_xattr_trusted(const char *upper) { - char tmp_path[PATH_MAX]; - strcpy(tmp_path, upper); - strcat(tmp_path, "/.xattr_test_XXXXXX.tmp"); - int tmp_file = mkstemps(tmp_path, 4); - if (tmp_file < 0) { return false; } - bool ret = real_check_xattr_trusted(tmp_path, tmp_file); - unlink(tmp_path); - return ret; -} - -int main(int argc, char *argv[]) { - - char *lower = NULL; - char *upper = NULL; - char *dir, *mnt = NULL; - - static struct option long_options[] = { - { "lowerdir", required_argument, 0, 'l' }, - { "upperdir", required_argument, 0, 'u' }, - { "mountdir", required_argument, 0, 'm' }, - { "lowernew", required_argument, 0, 'L' }, - { "uppernew", required_argument, 0, 'U' }, - { "yes", no_argument , 0, 'y' }, - { "help", no_argument , 0, 'h' }, - { "verbose", no_argument , 0, 'v' }, - { "brief", no_argument , 0, 'b' }, - { 0, 0, 0, 0 } - }; - - int opt = 0; - int long_index = 0; - while ((opt = getopt_long_only(argc, argv, "l:u:m:L:U:yhvb", long_options, &long_index)) != -1) { - switch (opt) { - case 'l': - lower = realpath(optarg, NULL); - if (lower) { vars[LOWERDIR] = lower; } - break; - case 'u': - upper = realpath(optarg, NULL); - if (upper) { vars[UPPERDIR] = upper; } - break; - case 'm': - mnt = realpath(optarg, NULL); - if (mnt) { vars[MOUNTDIR] = mnt; } - break; - case 'L': - directory_create("New lowerdir", optarg); - dir = realpath(optarg, NULL); - if (dir) { vars[LOWERNEW] = dir; } - break; - case 'U': - directory_create("New upperdir", optarg); - dir = realpath(optarg, NULL); - if (dir) { vars[UPPERNEW] = dir; } - break; - case 'y': - yes = true; - break; - case 'h': - print_help(argv[0]); - return EXIT_SUCCESS; - case 'v': - verbose = true; - brief = false; - break; - case 'b': - verbose = false; - brief = true; - break; - default: - fprintf(stderr, "Option %c is not supported.\n", opt); - goto see_help; - } - } - - if (!lower) { - fprintf(stderr, "Lower directory not specified.\n"); - goto see_help; - } - if (!directory_exists(lower)) { - fprintf(stderr, "Lower directory cannot be opened.\n"); - goto see_help; - } - if (!upper) { - fprintf(stderr, "Upper directory not specified.\n"); - goto see_help; - } - if (!directory_exists(upper)) { - fprintf(stderr, "Upper directory cannot be opened.\n"); - goto see_help; - } - if (!check_xattr_trusted(upper)) { - fprintf(stderr, "The program cannot write trusted.* xattr. Try run again as root.\n"); - return EXIT_FAILURE; - } - // Relax check for mounted overlay if we are not going to modify lowerdir/upperdir - if ((!vars[LOWERNEW] || !vars[UPPERNEW]) && check_mounted(lower, upper)) { - return EXIT_FAILURE; - } - - if (optind == argc - 1) { - int out; - char filename_template[] = "overlay-tools-XXXXXX.sh"; - FILE *script = NULL; - if (strcmp(argv[optind], "diff") == 0) { - out = diff(lower, upper); - } else if (strcmp(argv[optind], "vacuum") == 0) { - script = create_shell_script(filename_template); - if (script == NULL) { fprintf(stderr, "Script file cannot be created.\n"); return EXIT_FAILURE; } - out = vacuum(lower, upper, script); - } else if (strcmp(argv[optind], "merge") == 0) { - script = create_shell_script(filename_template); - if (script == NULL) { fprintf(stderr, "Script file cannot be created.\n"); return EXIT_FAILURE; } - out = merge(lower, upper, script); - } else if (strcmp(argv[optind], "deref") == 0) { - if (!mnt || !vars[UPPERNEW]) { fprintf(stderr, "'deref' command requires --uppernew and --mountdir.\n"); return EXIT_FAILURE; } - if (!directory_exists(mnt)) { - fprintf(stderr, "OverlayFS mount directory cannot be opened.\n"); - goto see_help; - } - script = create_shell_script(filename_template); - if (script == NULL) { fprintf(stderr, "Script file cannot be created.\n"); return EXIT_FAILURE; } - out = deref(mnt, upper, script); - } else { - fprintf(stderr, "Action not supported.\n"); - goto see_help; - } - if (script != NULL) { - printf("The script %s is created. Run the script to do the actual work please. Remember to run it when the OverlayFS is not mounted.\n", filename_template); - fclose(script); - } - if (out) { - fprintf(stderr, "Action aborted due to fatal error.\n"); - return EXIT_FAILURE; - } - return EXIT_SUCCESS; - } - - fprintf(stderr, "Please specify one action.\n"); - -see_help: - fprintf(stderr, "Try '%s --help' for more information.\n", argv[0]); - return EXIT_FAILURE; - -} diff --git a/overlayfs-tools/makefile b/overlayfs-tools/makefile deleted file mode 100755 index 963cc99..0000000 --- a/overlayfs-tools/makefile +++ /dev/null @@ -1,23 +0,0 @@ -CC = gcc -CFLAGS = -Wall -std=c99 -LDFLAGS = -lm -ifneq (,$(wildcard /etc/alpine-release)) - LDFLAGS += -lfts -endif - -all: overlayfs-tools - -overlayfs-tools: main.o logic.o sh.o - $(CC) main.o logic.o sh.o -o overlayfs-tools $(LDFLAGS) - -main.o: main.c logic.h - $(CC) $(CFLAGS) -c main.c - -logic.o: logic.c logic.h sh.h - $(CC) $(CFLAGS) -c logic.c - -sh.o: sh.c sh.h - $(CC) $(CFLAGS) -c sh.c - -clean: - $(RM) main.o logic.o sh.o overlayfs-tools diff --git a/overlayfs-tools/sh.c b/overlayfs-tools/sh.c deleted file mode 100755 index 98565ac..0000000 --- a/overlayfs-tools/sh.c +++ /dev/null @@ -1,98 +0,0 @@ -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include "sh.h" - -char * vars[NUM_VARS]; -const char * var_names[NUM_VARS] = { - "LOWERDIR", - "UPPERDIR", - "MOUNTDIR", - "LOWERNEW", - "UPPERNEW", -}; - -int quote(const char *filename, FILE *output); - -FILE* create_shell_script(char *tmp_path_buffer) { - int tmp_file = mkstemps(tmp_path_buffer, 3); // the 3 is for suffix length (".sh") - if (tmp_file < 0) { return NULL; } - fchmod(tmp_file, S_IRWXU); // chmod to 0700 - FILE* f = fdopen(tmp_file, "w"); - if (f == NULL) { return NULL; } - fprintf(f, "#!/usr/bin/env bash\n"); - fprintf(f, "set -x\n"); - time_t rawtime; - time (&rawtime); - fprintf(f, "# This shell script is generated by overlayfs-tools on %s\n", ctime (&rawtime)); - for (int i=0; i < NUM_VARS; i++) { - if (vars[i]) { - fprintf(f, "%s=", var_names[i]); - if (quote(vars[i], f) < 0) { return NULL; } - if (fputc('\n', f) == EOF) { return NULL; } - } - } - // Non-empty *NEW vars make a backup copy and override *DIR vars - if (vars[LOWERNEW]) { - fprintf(f, "rm -rf \"$LOWERNEW\"\n"); - fprintf(f, "cp -a \"$LOWERDIR\" \"$LOWERNEW\"\n"); - fprintf(f, "LOWERDIR="); - if (quote(vars[LOWERNEW], f) < 0) { return NULL; } - if (fputc('\n', f) == EOF) { return NULL; } - } - if (vars[UPPERNEW]) { - fprintf(f, "rm -rf \"$UPPERNEW\"\n"); - fprintf(f, "cp -a \"$UPPERDIR\" \"$UPPERNEW\"\n"); - fprintf(f, "UPPERDIR="); - if (quote(vars[UPPERNEW], f) < 0) { return NULL; } - if (fputc('\n', f) == EOF) { return NULL; } - } - return f; -} - -int quote(const char *filename, FILE *output) { - if (fputc('\'', output) == EOF) { return -1; } - for (const char *s = filename; *s != '\0'; s++) { - if (*s == '\'') { - if (fprintf(output, "'\"'\"'") < 0) { return -1; } - } else { - if (fputc(*s, output) == EOF) { return -1; } - } - } - if (fputc('\'', output) == EOF) { return -1; } - return 0; -} - -int substitue(char what, const char *filename, FILE *output) { - int i; - for (i=0; i < NUM_VARS; i++) - if (vars[i] && var_names[i][0] == what) - break; - if (i == NUM_VARS) { return -1; } - // filename prefix must match the var value - int prefix = strlen(vars[i]); - if (strncmp(filename, vars[i], prefix)) { return -1; } - filename += prefix; - fprintf(output, "\"$%s\"", var_names[i]); - return quote(filename, output); -} - -int command(FILE *output, const char *command_format, ...) { - va_list arg; - va_start(arg, command_format); - for (size_t i = 0; command_format[i] != '\0'; i++) { - if (command_format[i] == '%') { - const char *s = va_arg(arg, char *); - if (substitue(command_format[++i], s, output) < 0) { return -1; } - } else { - if (fputc(command_format[i], output) == EOF) { return -1; } - } - } - va_end(arg); - if (fputc('\n', output) == EOF) { return -1; } - return 0; -} diff --git a/overlayfs-tools/sh.h b/overlayfs-tools/sh.h deleted file mode 100755 index 19d9bdb..0000000 --- a/overlayfs-tools/sh.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef OVERLAYFS_TOOLS_SH_H -#define OVERLAYFS_TOOLS_SH_H - -enum { - LOWERDIR, - UPPERDIR, - MOUNTDIR, - LOWERNEW, - UPPERNEW, - NUM_VARS -}; - -extern const char *var_names[NUM_VARS]; -extern char *vars[NUM_VARS]; - -FILE* create_shell_script(char *tmp_path_buffer); - -int command(FILE *output, const char *command_format, ...); - -#endif //OVERLAYFS_TOOLS_SH_H From 6bee27bb0d7df03cee93fee2da5d00e4e6ea59b8 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Thu, 24 Apr 2025 00:05:46 +0100 Subject: [PATCH 115/121] feat: replace fedora-39 with fedora-42, fix package installer --- blend | 4 ++-- blend-settings/src/package-installer.html | 11 ++++++----- blend-settings/src/pages/containers.html | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/blend b/blend index 014989c..98335d7 100755 --- a/blend +++ b/blend @@ -87,9 +87,9 @@ def error(err): distro_map = { - 'arch-linux': 'docker.io/library/archlinux', + 'arch-linux': 'quay.io/toolbx/arch-toolbox:latest', 'debian': 'quay.io/toolbx-images/debian-toolbox:testing', - 'fedora-39': 'registry.fedoraproject.org/fedora-toolbox:39', + 'fedora-42': 'quay.io/fedora/fedora-toolbox:42', 'centos': 'quay.io/toolbx-images/centos-toolbox:latest', 'ubuntu-22.04': 'quay.io/toolbx/ubuntu-toolbox:22.04', 'ubuntu-24.04': 'quay.io/toolbx/ubuntu-toolbox:24.04', diff --git a/blend-settings/src/package-installer.html b/blend-settings/src/package-installer.html index a20640b..a2cb791 100644 --- a/blend-settings/src/package-installer.html +++ b/blend-settings/src/package-installer.html @@ -88,7 +88,7 @@ - \ No newline at end of file + diff --git a/blend-settings/src/pages/containers.html b/blend-settings/src/pages/containers.html index 935a85a..c4f8ca1 100644 --- a/blend-settings/src/pages/containers.html +++ b/blend-settings/src/pages/containers.html @@ -13,7 +13,7 @@ - + From 7f59891a4e6e3657c3625126102c79fc9397e5c5 Mon Sep 17 00:00:00 2001 From: askiiart Date: Wed, 23 Apr 2025 13:42:17 -0500 Subject: [PATCH 116/121] only use `-it` for ttys --- blend | 63 +++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/blend b/blend index 98335d7..716ddef 100755 --- a/blend +++ b/blend @@ -19,7 +19,7 @@ import os import sys -import glob +from sys import stdout import time import shutil import socket @@ -238,14 +238,22 @@ def host_get_output(cmd): return subprocess.run(['bash', '-c', cmd], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode('UTF-8').strip() -def core_get_retcode(cmd): return subprocess.run(['podman', 'exec', '--user', getpass.getuser(), '-it', args.container_name, 'bash', '-c', cmd], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode +def core_get_retcode(cmd): + # only use `-it` if stdout is a tty + cmd = ['podman', 'exec', '--user', getpass.getuser(), '-it', + args.container_name, 'bash', '-c', cmd] + cmd = [x for x in cmd if x != '-it' or stdout.isatty()] + return subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode def core_run_container(cmd): if os.getcwd() == os.path.expanduser('~') or os.getcwd().startswith(os.path.expanduser('~') + '/'): - subprocess.call(['podman', 'exec', '--user', getpass.getuser(), - '-w', os.getcwd(), '-it', args.container_name, 'bash', '-c', cmd]) + # only use `-it` if stdout is a tty + cmd = ['podman', 'exec', '--user', getpass.getuser(), + '-w', os.getcwd(), '-it', args.container_name, 'bash', '-c', cmd] + cmd = [x for x in cmd if x != '-it' or stdout.isatty()] + + subprocess.call(cmd) def core_install_pkg(pkg): @@ -424,25 +432,46 @@ def enter_container(): if not os.environ.get('BLEND_COMMAND'): if args.pkg == []: if os.getcwd() == os.path.expanduser('~') or os.getcwd().startswith(os.path.expanduser('~') + '/'): - exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, - '-w', os.getcwd(), '-it', args.container_name, 'bash'])) + # only use `-it` if stdout is a tty + cmd = [*sudo, 'podman', 'exec', *podman_args, + '-w', os.getcwd(), '-it', args.container_name, 'bash'] + cmd = [x for x in cmd if x != '-it' or stdout.isatty()] + exit(subprocess.call(cmd)) + else: - exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', - '/run/host' + os.getcwd(), '-it', args.container_name, 'bash'])) + # only use `-it` if stdout is a tty + cmd = [*sudo, 'podman', 'exec', *podman_args, '-w', + '/run/host' + os.getcwd(), '-it', args.container_name, 'bash'] + cmd = [x for x in cmd if x != '-it' or stdout.isatty()] + exit(subprocess.call(cmd)) else: if os.getcwd() == os.path.expanduser('~') or os.getcwd().startswith(os.path.expanduser('~') + '/'): - exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, - '-w', os.getcwd(), '-it', args.container_name, *args.pkg])) + # only use `-it` if stdout is a tty + cmd = [*sudo, 'podman', 'exec', *podman_args, + '-w', os.getcwd(), '-it', args.container_name, *args.pkg] + cmd = [x for x in cmd if x != '-it' or stdout.isatty()] + exit(subprocess.call(cmd)) else: - exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', - '/run/host' + os.getcwd(), '-it', args.container_name, *args.pkg])) + # only use `-it` if stdout is a tty + cmd = [*sudo, 'podman', 'exec', *podman_args, '-w', + '/run/host' + os.getcwd(), '-it', args.container_name, *args.pkg] + cmd = [x for x in cmd if x != '-it' or stdout.isatty()] + exit(subprocess.call(cmd)) + else: if os.getcwd() == os.path.expanduser('~') or os.getcwd().startswith(os.path.expanduser('~') + '/'): - exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', os.getcwd( - ), '-it', args.container_name, 'bash', '-c', os.environ.get('BLEND_COMMAND')])) + # only use `-it` if stdout is a tty + cmd = [*sudo, 'podman', 'exec', *podman_args, '-w', os.getcwd(), '-it', + args.container_name, 'bash', '-c', os.environ.get('BLEND_COMMAND')] + cmd = [x for x in cmd if x != '-it' or stdout.isatty()] + exit(subprocess.call(cmd)) + else: - exit(subprocess.call([*sudo, 'podman', 'exec', *podman_args, '-w', - '/run/host' + os.getcwd(), '-it', args.container_name, 'bash'])) + # only use `-it` if stdout is a tty + cmd = [*sudo, 'podman', 'exec', *podman_args, '-w', + '/run/host' + os.getcwd(), '-it', args.container_name, 'bash'] + cmd = [x for x in cmd if x != '-it' or stdout.isatty()] + exit(subprocess.call(cmd)) def create_container(): From 6a6680403fb39782b87e13bd49996dc5a87a440b Mon Sep 17 00:00:00 2001 From: askiiart Date: Thu, 24 Apr 2025 11:15:12 -0500 Subject: [PATCH 117/121] DRY --- blend | 65 ++++++++++++++++++++++------------------------------------- 1 file changed, 24 insertions(+), 41 deletions(-) diff --git a/blend b/blend index 716ddef..3d20d54 100755 --- a/blend +++ b/blend @@ -83,6 +83,13 @@ def error(err): print(colors.bold + colors.fg.red + '>> e: ' + colors.reset + colors.bold + err + colors.reset) + +def podman_it_remover(cmd: list): + ''' + Removes `-it` from a podman command if stdout is not a tty + ''' + return [x for x in cmd if x != '-it' or stdout.isatty()] + # END @@ -239,21 +246,15 @@ def host_get_output(cmd): return subprocess.run(['bash', '-c', cmd], def core_get_retcode(cmd): - # only use `-it` if stdout is a tty - cmd = ['podman', 'exec', '--user', getpass.getuser(), '-it', - args.container_name, 'bash', '-c', cmd] - cmd = [x for x in cmd if x != '-it' or stdout.isatty()] - return subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode + podman_command = podman_it_remover(['podman', 'exec', '--user', getpass.getuser(), '-it', + args.container_name, 'bash', '-c', cmd]) + return subprocess.run(podman_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode def core_run_container(cmd): if os.getcwd() == os.path.expanduser('~') or os.getcwd().startswith(os.path.expanduser('~') + '/'): - # only use `-it` if stdout is a tty - cmd = ['podman', 'exec', '--user', getpass.getuser(), - '-w', os.getcwd(), '-it', args.container_name, 'bash', '-c', cmd] - cmd = [x for x in cmd if x != '-it' or stdout.isatty()] - - subprocess.call(cmd) + subprocess.call(podman_it_remover(['podman', 'exec', '--user', getpass.getuser(), + '-w', os.getcwd(), '-it', args.container_name, 'bash', '-c', cmd])) def core_install_pkg(pkg): @@ -432,46 +433,28 @@ def enter_container(): if not os.environ.get('BLEND_COMMAND'): if args.pkg == []: if os.getcwd() == os.path.expanduser('~') or os.getcwd().startswith(os.path.expanduser('~') + '/'): - # only use `-it` if stdout is a tty - cmd = [*sudo, 'podman', 'exec', *podman_args, - '-w', os.getcwd(), '-it', args.container_name, 'bash'] - cmd = [x for x in cmd if x != '-it' or stdout.isatty()] - exit(subprocess.call(cmd)) + exit(subprocess.call(podman_it_remover([*sudo, 'podman', 'exec', *podman_args, + '-w', os.getcwd(), '-it', args.container_name, 'bash']))) else: - # only use `-it` if stdout is a tty - cmd = [*sudo, 'podman', 'exec', *podman_args, '-w', - '/run/host' + os.getcwd(), '-it', args.container_name, 'bash'] - cmd = [x for x in cmd if x != '-it' or stdout.isatty()] - exit(subprocess.call(cmd)) + exit(subprocess.call(podman_it_remover([*sudo, 'podman', 'exec', *podman_args, '-w', + '/run/host' + os.getcwd(), '-it', args.container_name, 'bash']))) else: if os.getcwd() == os.path.expanduser('~') or os.getcwd().startswith(os.path.expanduser('~') + '/'): - # only use `-it` if stdout is a tty - cmd = [*sudo, 'podman', 'exec', *podman_args, - '-w', os.getcwd(), '-it', args.container_name, *args.pkg] - cmd = [x for x in cmd if x != '-it' or stdout.isatty()] - exit(subprocess.call(cmd)) + exit(subprocess.call(podman_it_remover([*sudo, 'podman', 'exec', *podman_args, + '-w', os.getcwd(), '-it', args.container_name, *args.pkg]))) else: - # only use `-it` if stdout is a tty - cmd = [*sudo, 'podman', 'exec', *podman_args, '-w', - '/run/host' + os.getcwd(), '-it', args.container_name, *args.pkg] - cmd = [x for x in cmd if x != '-it' or stdout.isatty()] - exit(subprocess.call(cmd)) + exit(subprocess.call(podman_it_remover([*sudo, 'podman', 'exec', *podman_args, '-w', + '/run/host' + os.getcwd(), '-it', args.container_name, *args.pkg]))) else: if os.getcwd() == os.path.expanduser('~') or os.getcwd().startswith(os.path.expanduser('~') + '/'): - # only use `-it` if stdout is a tty - cmd = [*sudo, 'podman', 'exec', *podman_args, '-w', os.getcwd(), '-it', - args.container_name, 'bash', '-c', os.environ.get('BLEND_COMMAND')] - cmd = [x for x in cmd if x != '-it' or stdout.isatty()] - exit(subprocess.call(cmd)) + exit(subprocess.call(podman_it_remover([*sudo, 'podman', 'exec', *podman_args, '-w', os.getcwd(), '-it', + args.container_name, 'bash', '-c', os.environ.get('BLEND_COMMAND')]))) else: - # only use `-it` if stdout is a tty - cmd = [*sudo, 'podman', 'exec', *podman_args, '-w', - '/run/host' + os.getcwd(), '-it', args.container_name, 'bash'] - cmd = [x for x in cmd if x != '-it' or stdout.isatty()] - exit(subprocess.call(cmd)) + exit(subprocess.call(podman_it_remover([*sudo, 'podman', 'exec', *podman_args, '-w', + '/run/host' + os.getcwd(), '-it', args.container_name, 'bash']))) def create_container(): From 021d2f0aa5e80b7df9f0e1486157b33b9965816e Mon Sep 17 00:00:00 2001 From: askiiart Date: Fri, 25 Apr 2025 08:52:44 -0500 Subject: [PATCH 118/121] add ability to load custom images from file (also not hardcoded anymore) --- blend | 17 +++++++++-------- images.example.yaml | 3 +++ images.yaml | 6 ++++++ 3 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 images.example.yaml create mode 100644 images.yaml diff --git a/blend b/blend index 3d20d54..a5f8ecb 100755 --- a/blend +++ b/blend @@ -27,6 +27,7 @@ import getpass import pexpect import argparse import subprocess +import yaml __version = '2.0.0' @@ -93,14 +94,14 @@ def podman_it_remover(cmd: list): # END -distro_map = { - 'arch-linux': 'quay.io/toolbx/arch-toolbox:latest', - 'debian': 'quay.io/toolbx-images/debian-toolbox:testing', - 'fedora-42': 'quay.io/fedora/fedora-toolbox:42', - 'centos': 'quay.io/toolbx-images/centos-toolbox:latest', - 'ubuntu-22.04': 'quay.io/toolbx/ubuntu-toolbox:22.04', - 'ubuntu-24.04': 'quay.io/toolbx/ubuntu-toolbox:24.04', -} +distro_map = yaml.safe_load(open('/usr/share/blend/images.yaml')) +if os.path.exists(f'/etc/blend/images.yaml'): + distro_map += yaml.safe_load( + open(f'{os.getenv('XDG_CONFIG_HOME')}/blend/images.yaml')) + +if os.path.exists(f'{os.getenv('XDG_CONFIG_HOME')}/blend/images.yaml'): + distro_map += yaml.safe_load( + open(f'{os.getenv('XDG_CONFIG_HOME')}/blend/images.yaml')) default_distro = 'arch-linux' diff --git a/images.example.yaml b/images.example.yaml new file mode 100644 index 0000000..1927866 --- /dev/null +++ b/images.example.yaml @@ -0,0 +1,3 @@ +# Here you can put custom images to be used for blend +# for example: +# opensuse: quay.io/exampleauthor/opensuse:15 diff --git a/images.yaml b/images.yaml new file mode 100644 index 0000000..84a9f79 --- /dev/null +++ b/images.yaml @@ -0,0 +1,6 @@ +arch-linux: quay.io/toolbx/arch-toolbox:latest +debian: quay.io/toolbx-images/debian-toolbox:testing +fedora-42: quay.io/fedora/fedora-toolbox:42 +centos: quay.io/toolbx-images/centos-toolbox:latest +ubuntu-22.04: quay.io/toolbx/ubuntu-toolbox:22.04 +ubuntu-24.04: quay.io/toolbx/ubuntu-toolbox:24.04 From b771df150efd2fff1ff80c42fb0c5adff11db06b Mon Sep 17 00:00:00 2001 From: askiiart Date: Fri, 25 Apr 2025 10:39:14 -0500 Subject: [PATCH 119/121] add aliases and redo the format a bunch --- blend | 51 +++++++++++++++++++++++++++++++++++++-------- images.example.yaml | 3 --- images.yaml | 24 +++++++++++++++------ 3 files changed, 60 insertions(+), 18 deletions(-) delete mode 100644 images.example.yaml diff --git a/blend b/blend index a5f8ecb..204e5af 100755 --- a/blend +++ b/blend @@ -93,20 +93,53 @@ def podman_it_remover(cmd: list): # END +# TODO: fix temp paths before committing -distro_map = yaml.safe_load(open('/usr/share/blend/images.yaml')) -if os.path.exists(f'/etc/blend/images.yaml'): - distro_map += yaml.safe_load( - open(f'{os.getenv('XDG_CONFIG_HOME')}/blend/images.yaml')) -if os.path.exists(f'{os.getenv('XDG_CONFIG_HOME')}/blend/images.yaml'): - distro_map += yaml.safe_load( - open(f'{os.getenv('XDG_CONFIG_HOME')}/blend/images.yaml')) +def image_data_from_dict(dictionary): + ''' + Returns a distro map and aliases from a dict in this format: + + ``` + {'arch-linux': {'image': 'quay.io/toolbx/arch-toolbox:latest', 'aliases': ['arch']}} + ``` + + which is loaded in from yaml config files in this format: + + ```yaml + arch-linux: + image: "quay.io/toolbx/arch-toolbox:latest" + aliases: + - arch + ``` + ''' + distro_map = {} + aliases = {} + + for distro in dictionary.keys(): + distro_map[distro] = dictionary[distro]['image'] + try: + aliases[distro] = dictionary[distro]['aliases'] + except KeyError: + pass + + return (distro_map, aliases) + + +distro_map, aliases = image_data_from_dict( + yaml.safe_load(open('/usr/share/blend/images.yaml'))) +tmp = image_data_from_dict(yaml.safe_load(open('system.yaml'))['images']) +distro_map += tmp[0] +aliases += tmp[1] +tmp += image_data_from_dict(yaml.safe_load( + open(f'{os.getenv('XDG_CONFIG_HOME')}/blend/images.yaml'))) +distro_map += tmp[0] +aliases += tmp[1] default_distro = 'arch-linux' -def get_distro(): +def get_image(): try: return distro_map[args.distro] except: @@ -222,7 +255,7 @@ def core_create_container(): '--userns', 'keep-id', '--annotation', 'run.oci.keep_original_groups=1']) - podman_command.extend([get_distro()]) + podman_command.extend([get_image()]) # User (for init-blend) podman_command.extend(['--uid', str(os.geteuid())]) diff --git a/images.example.yaml b/images.example.yaml deleted file mode 100644 index 1927866..0000000 --- a/images.example.yaml +++ /dev/null @@ -1,3 +0,0 @@ -# Here you can put custom images to be used for blend -# for example: -# opensuse: quay.io/exampleauthor/opensuse:15 diff --git a/images.yaml b/images.yaml index 84a9f79..8753f84 100644 --- a/images.yaml +++ b/images.yaml @@ -1,6 +1,18 @@ -arch-linux: quay.io/toolbx/arch-toolbox:latest -debian: quay.io/toolbx-images/debian-toolbox:testing -fedora-42: quay.io/fedora/fedora-toolbox:42 -centos: quay.io/toolbx-images/centos-toolbox:latest -ubuntu-22.04: quay.io/toolbx/ubuntu-toolbox:22.04 -ubuntu-24.04: quay.io/toolbx/ubuntu-toolbox:24.04 +arch-linux: + image: quay.io/toolbx/arch-toolbox:latest + aliases: + - arch +debian: + image: quay.io/toolbx-images/debian-toolbox:testing +fedora-42: + image: quay.io/fedora/fedora-toolbox:42 + aliases: + - fedora +centos: + image: quay.io/toolbx-images/centos-toolbox:latest +ubuntu-22.04: + image: quay.io/toolbx/ubuntu-toolbox:22.04 +ubuntu-24.04: + image: quay.io/toolbx/ubuntu-toolbox:24.04 + aliases: + - ubuntu From da373914d68273cd77035a29b49f552de917cd6f Mon Sep 17 00:00:00 2001 From: askiiart Date: Fri, 25 Apr 2025 10:40:10 -0500 Subject: [PATCH 120/121] fix paths --- blend | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/blend b/blend index 204e5af..58a18b2 100755 --- a/blend +++ b/blend @@ -128,13 +128,14 @@ def image_data_from_dict(dictionary): distro_map, aliases = image_data_from_dict( yaml.safe_load(open('/usr/share/blend/images.yaml'))) -tmp = image_data_from_dict(yaml.safe_load(open('system.yaml'))['images']) -distro_map += tmp[0] -aliases += tmp[1] -tmp += image_data_from_dict(yaml.safe_load( - open(f'{os.getenv('XDG_CONFIG_HOME')}/blend/images.yaml'))) +tmp = image_data_from_dict(yaml.safe_load(open('/system.yaml'))['images']) distro_map += tmp[0] aliases += tmp[1] +if os.path.exists(f'{os.getenv('XDG_CONFIG_HOME')}/blend/images.yaml'): + tmp = image_data_from_dict(yaml.safe_load( + open(f'{os.getenv('XDG_CONFIG_HOME')}/blend/images.yaml'))) + distro_map += tmp[0] + aliases += tmp[1] default_distro = 'arch-linux' From f5bd881b5d547cb1d2e53be079ae24405014a04e Mon Sep 17 00:00:00 2001 From: askiiart Date: Fri, 25 Apr 2025 10:49:37 -0500 Subject: [PATCH 121/121] handle aliases --- blend | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/blend b/blend index 58a18b2..32de591 100755 --- a/blend +++ b/blend @@ -293,9 +293,6 @@ def core_run_container(cmd): def core_install_pkg(pkg): - if args.distro == 'arch': - args.distro = 'arch-linux' - if args.distro.startswith('fedora-'): if args.noconfirm == True: core_run_container(f'sudo dnf -y install {pkg}') @@ -322,9 +319,6 @@ def core_install_pkg(pkg): def core_remove_pkg(pkg): - if args.distro == 'arch': - args.distro = 'arch-linux' - if args.distro.startswith('fedora-'): if args.noconfirm == True: core_run_container(f'sudo dnf -y remove {pkg}') @@ -344,9 +338,6 @@ def core_remove_pkg(pkg): def core_search_pkg(pkg): - if args.distro == 'arch': - args.distro = 'arch-linux' - if args.distro.startswith('fedora-'): core_run_container(f'dnf search {pkg}') elif args.distro == 'arch-linux': @@ -358,9 +349,6 @@ def core_search_pkg(pkg): def core_show_pkg(pkg): - if args.distro == 'arch': - args.distro = 'arch-linux' - if args.distro.startswith('fedora-'): core_run_container(f'dnf info {pkg}') elif args.distro == 'arch-linux': @@ -415,9 +403,6 @@ def show_blend(): def sync_blends(): - if args.distro == 'arch': - args.distro = 'arch-linux' - if args.distro.startswith('fedora-'): core_run_container(f'dnf makecache') elif args.distro == 'arch-linux': @@ -427,9 +412,6 @@ def sync_blends(): def update_blends(): - if args.distro == 'arch': - args.distro = 'arch-linux' - if args.distro.startswith('fedora-'): if args.noconfirm == True: core_run_container(f'sudo dnf -y upgrade') @@ -494,7 +476,6 @@ def enter_container(): def create_container(): for container in args.pkg: - container = 'ubuntu-24.04' if container == 'ubuntu-24.04-lts' else container args.container_name = container if container in distro_map.keys() and distro_input == None: args.distro = container @@ -578,6 +559,10 @@ if len(sys.argv) == 1: exit() args = parser.parse_intermixed_args() +if args.distro not in distro_map.keys(): + for (distro, al) in aliases: + if args.distro in al: + args.distro = distro command = command_map[args.command]
-
+
-
@@ -41,6 +41,10 @@ const $ = require("jquery") + if (fs.existsSync('/usr/bin/waydroid')) { + document.getElementById('android-button').classList.remove('d-none') + } + function page(page) { switch (page) { case 'containers': diff --git a/blend-settings/src/internal/js/android.js b/blend-settings/src/internal/js/android.js index a815be1..b6088a2 100644 --- a/blend-settings/src/internal/js/android.js +++ b/blend-settings/src/internal/js/android.js @@ -1,135 +1,136 @@ -function rollback() { - let rollback_worker = new Worker( - `data:text/javascript, - let s = require('child_process').spawnSync('pkexec', ['blend-system', 'rollback']).status - if (s === 0) { - postMessage('success') - } else { - postMessage('failure') - } - ` - ) - rollback_worker.onmessage = e => { - if (e.data == 'success') { - document.getElementById('rollback-btn').outerHTML = - '' - } else { - document.getElementById('rollback-btn').outerHTML = - '' - setTimeout(() => document.getElementById('rollback-btn').outerHTML = - '', 2000) - } - } -} - -function undo_rollback() { - let undo_rollback_worker = new Worker( - `data:text/javascript, - let s = require('child_process').spawnSync('pkexec', ['rm', '-f', '/blend/states/.load_prev_state']).status - if (s === 0) { - postMessage('success') - } else { - postMessage('failure') - } - ` - ) - undo_rollback_worker.onmessage = e => { - if (e.data == 'success') { - document.getElementById('rollback-btn').outerHTML = - '' - } else { - document.getElementById('rollback-btn').outerHTML = - '' - setTimeout(() => document.getElementById('rollback-btn').outerHTML = - '', 2000) - } - } -} - function init_waydroid() { document.getElementById('initialize-btn').outerHTML = '' let init_worker = new Worker( `data:text/javascript, require('child_process').spawnSync('pkexec', ['waydroid', 'init']) + require('child_process').spawnSync('pkexec', ['systemctl', 'enable', '--now', 'waydroid-container']) require('child_process').spawn('sh', ['-c', 'waydroid session start & disown']) setTimeout(() => { require('child_process').spawnSync('pkexec', ['waydroid', 'shell', 'pm', 'disable', 'com.android.inputmethod.latin']) require('child_process').spawnSync('waydroid', ['prop', 'set', 'persist.waydroid.multi_windows', 'true']) - postMessage('success') + if (require('child_process').spawnSync('sh', ['-c', 'LC_ALL=C glxinfo | grep "^OpenGL renderer string: "']).stdout.includes('NVIDIA')) { + require('child_process').spawnSync('sh', ['-c', 'echo "ro.hardware.gralloc=default" | pkexec tee -a /var/lib/waydroid/waydroid.cfg']) + require('child_process').spawnSync('sh', ['-c', 'echo "ro.hardware.egl=swiftshader" | pkexec tee -a /var/lib/waydroid/waydroid.cfg']) + } + require('child_process').spawn('sh', ['-c', 'pkexec waydroid upgrade -o; waydroid session stop; waydroid session start']) + setTimeout(() => { postMessage('success') }, 1000) }, 2000) ` ) init_worker.onmessage = e => { if (e.data == 'success') { - document.getElementById('init-waydroid').classList.add('d-none') + document.getElementById('waydroid-initialize-settings').classList.add('d-none') document.getElementById('waydroid-initialized-settings').classList.remove('d-none') } } } -function enable_multi_window() { - document.getElementById('multiwindow-btn').outerHTML = - '' - let multi_window_worker = new Worker( +function install_aurora_store() { + document.getElementById('aurora-store-btn').outerHTML = + `` + let aurora_store_worker = new Worker( `data:text/javascript, - require('child_process').spawn('sh', ['-c', 'waydroid session start & disown']) - setTimeout(() => { require('child_process').spawnSync('waydroid', ['prop', 'set', 'persist.waydroid.multi_windows', 'true']); require('child_process').spawn('sh', ['-c', 'waydroid session stop']); postMessage('success') }, 500) + require('child_process').spawnSync('sh', ['-c', 'mkdir -p ~/.cache/blend-settings; rm -f ~/.cache/blend-settings/aurora.apk']) + let s1 = require('child_process').spawnSync('sh', ['-c', 'wget -O ~/.cache/blend-settings/aurora.apk https://gitlab.com/AuroraOSS/AuroraStore/uploads/bbc1bd5a77ab2b40bbf288ccbef8d1f0/AuroraStore_4.1.1.apk']).status + if (s1 != 0) { + postMessage('failed') + } else { + require('child_process').spawn('waydroid', ['session', 'start']) + setTimeout(() => { + require('child_process').spawnSync('sh', ['-c', 'waydroid app install ~/.cache/blend-settings/aurora.apk']) + setTimeout(() => postMessage('success'), 200) + }, 2000) + } ` ) - multi_window_worker.onmessage = e => { + aurora_store_worker.onmessage = e => { if (e.data == 'success') { - document.getElementById('multiwindow-btn').outerHTML = - '' + document.getElementById('aurora-store-btn').outerHTML = + `` + } else if (e.data == 'failed') { + document.getElementById('aurora-store-btn').outerHTML = + `` + setTimeout(() => { + document.getElementById('aurora-store-btn').outerHTML = + `` + }, 2000) } } } -function disable_multi_window() { - document.getElementById('multiwindow-btn').outerHTML = - '' - let multi_window_worker = new Worker( +function install_f_droid() { + document.getElementById('f-droid-btn').outerHTML = + `` + let f_droid_worker = new Worker( `data:text/javascript, - require('child_process').spawn('sh', ['-c', 'waydroid session start & disown']) - setTimeout(() => { require('child_process').spawnSync('waydroid', ['prop', 'set', 'persist.waydroid.multi_windows', 'false']); require('child_process').spawn('sh', ['-c', 'waydroid session stop']); postMessage('success') }, 500) + require('child_process').spawnSync('sh', ['-c', 'mkdir -p ~/.cache/blend-settings; rm -f ~/.cache/blend-settings/f-droid.apk']) + let s1 = require('child_process').spawnSync('sh', ['-c', 'wget -O ~/.cache/blend-settings/f-droid.apk https://f-droid.org/F-Droid.apk']).status + if (s1 != 0) { + postMessage('failed') + } else { + require('child_process').spawn('waydroid', ['session', 'start']) + setTimeout(() => { + require('child_process').spawnSync('sh', ['-c', 'waydroid app install ~/.cache/blend-settings/f-droid.apk']) + setTimeout(() => postMessage('success'), 200) + }, 2000) + } ` ) - multi_window_worker.onmessage = e => { + f_droid_worker.onmessage = e => { if (e.data == 'success') { - document.getElementById('multiwindow-btn').outerHTML = - '' + document.getElementById('f-droid-btn').outerHTML = + `` + } else if (e.data == 'failed') { + document.getElementById('f-droid-btn').outerHTML = + `` + setTimeout(() => { + document.getElementById('f-droid-btn').outerHTML = + `` + }, 2000) } } } -function check_multi_window_enabled() { - let check_worker = new Worker( - `data:text/javascript, - require('child_process').spawn('sh', ['-c', 'waydroid session start & disown']) - setTimeout(() => { let val = require('child_process').spawnSync('waydroid', ['prop', 'get', 'persist.waydroid.multi_windows']).stdout; postMessage(val) }, 500) - ` - ) - check_worker.onmessage = e => { - if (new TextDecoder("utf-8").decode(e.data).trim() == 'true') { - document.getElementById('multiwindow-btn').outerHTML = - '' - } else { - document.getElementById('multiwindow-btn').outerHTML = - '' - } - } +function waydroid_open_settings() { + require('child_process').spawn('waydroid', ['app', 'launch', 'com.android.settings']) } require('fs').stat('/var/lib/waydroid', (err, stat) => { if (err == null) { document.getElementById('waydroid-initialize-settings').classList.add('d-none') document.getElementById('waydroid-initialized-settings').classList.remove('d-none') + if (require('child_process').spawnSync('sh', ['-c', 'LC_ALL=C glxinfo | grep "^OpenGL renderer string: "']).stdout.includes('NVIDIA')) { + document.getElementById('nvidia-warning-installed').classList.remove('d-none') + } + require('child_process').spawn('waydroid', ['session', 'start']) + setTimeout(() => { + if (require('child_process').spawnSync('waydroid', ['app', 'list']).stdout.includes('com.aurora.store')) { + document.getElementById('aurora-store-btn').outerHTML = + `` + } + if (require('child_process').spawnSync('waydroid', ['app', 'list']).stdout.includes('org.fdroid.fdroid')) { + document.getElementById('f-droid-btn').outerHTML = + `` + } + }, 1000) + } else { + if (require('child_process').spawnSync('sh', ['-c', 'LC_ALL=C glxinfo | grep "^OpenGL renderer string: "']).stdout.includes('NVIDIA')) { + document.getElementById('nvidia-warning').classList.remove('d-none') + } } }) -check_state_creation() -check_rollback() - $('#automatic-state-toggle').on('change', () => { if (!document.getElementById('automatic-state-toggle').checked) { let enable_autostate_worker = new Worker( diff --git a/blend-settings/src/internal/js/system.js b/blend-settings/src/internal/js/system.js index 62625ac..99b2cd2 100644 --- a/blend-settings/src/internal/js/system.js +++ b/blend-settings/src/internal/js/system.js @@ -47,6 +47,8 @@ function undo_rollback() { } function save_state() { + $("#settings-tabs").find("*").prop('disabled', true) + let save_state_worker = new Worker( `data:text/javascript, let s = require('child_process').spawnSync('pkexec', ['blend-system', 'save-state']).status @@ -61,11 +63,13 @@ function save_state() { if (e.data == 'success') { document.getElementById('save-state-btn').outerHTML = '' + $("#settings-tabs").find("*").prop('disabled', false) setTimeout(() => document.getElementById('save-state-btn').outerHTML = '', 2000) } else { document.getElementById('save-state-btn').outerHTML = '' + $("#settings-tabs").find("*").prop('disabled', false) setTimeout(() => document.getElementById('save-state-btn').outerHTML = '', 2000) } diff --git a/blend-settings/src/pages/android.html b/blend-settings/src/pages/android.html index da4b9d4..e039ff3 100644 --- a/blend-settings/src/pages/android.html +++ b/blend-settings/src/pages/android.html @@ -1,12 +1,12 @@
-
-
+
+
- Initialize Android App Support -

Initialize WayDroid to be able to run Android apps.

+ Initialize Android app support +

You may be asked to enter your password repeatedly to initialize WayDroid.

@@ -16,57 +16,34 @@
+
+

Since you're using an NVIDIA GPU, + Android apps will use software rendering.

+

After initializing WayDroid, you can install a store (or many) of your choice.

diff --git a/blend-settings/src/pages/containers.html b/blend-settings/src/pages/containers.html index 95ebfc7..130da17 100644 --- a/blend-settings/src/pages/containers.html +++ b/blend-settings/src/pages/containers.html @@ -1,8 +1,8 @@
Containers -

You can install any app from any of the supported distributions (Arch, Fedora, and Ubuntu). Any apps you install in them will appear as regular applications on your system, and so will any binaries. In case a binary is common between two containers, the binary from the most recently created container will be exported. You can override this by rearranging (dragging) the containers below to select the priority that should be assigned to each container.

-
+

You can install any app from any of the supported distributions (Arch, Fedora, and Ubuntu). Apps you install will appear as regular applications on your system (as well as binaries and package managers). You can override the priority in which common binaries are made available on the system by rearranging (dragging) the containers below to select the priority that should be assigned to each container.

+
@@ -18,6 +18,7 @@
Create new container +

Create a container for each distribution to be able to use their package managers and other binaries directly from a terminal.

diff --git a/blend-settings/src/pages/system.html b/blend-settings/src/pages/system.html index 4e4b9a3..800a0fa 100644 --- a/blend-settings/src/pages/system.html +++ b/blend-settings/src/pages/system.html @@ -7,13 +7,12 @@
Disable automatic state creation -

blendOS creates copies of apps and config every 6 hours (and keeps the - last two).

+

blendOS creates copies of apps and config every 12 hours (and keeps the + previous one).

- +
@@ -26,7 +25,8 @@
- +
diff --git a/blend-settings/src/pages/terminal.html b/blend-settings/src/pages/terminal.html index 1b81b3c..0f89938 100644 --- a/blend-settings/src/pages/terminal.html +++ b/blend-settings/src/pages/terminal.html @@ -80,6 +80,10 @@ fit.fit() ipc.on("terminal.incomingData", (event, data) => { + fit.fit(); + term.resize(term.cols, term.rows) + ipc.send("terminal.resize", [term.cols, term.rows]) + term.write(data); }); diff --git a/blend-system b/blend-system index 738866a..885e382 100755 --- a/blend-system +++ b/blend-system @@ -99,7 +99,7 @@ def autosave_state(): while True: if not os.path.isfile('/blend/states/.disable_states'): save_state() - time.sleep(6*60*60) # XXX: make this configurable + time.sleep(12*60*60) # XXX: make this configurable def toggle_states(): if os.path.isfile('/blend/states/.disable_states'): @@ -125,7 +125,6 @@ description = f''' {colors.bold}{colors.fg.purple}available commands{colors.reset}: {colors.bold}help{colors.reset} Show this help message and exit. {colors.bold}version{colors.reset} Show version information and exit. - {colors.bold}load-overlay{colors.reset} Load the current overlay. {colors.bold}save-state{colors.reset} Save the current state (backup). {colors.bold}toggle-states{colors.reset} Enable/disable automatic state creation (you can still manually save states). {colors.bold}rollback{colors.reset} Rollback to previous state. @@ -142,7 +141,6 @@ parser = argparse.ArgumentParser(description=description, usage=argparse.SUPPRES epilog=epilog, formatter_class=argparse.RawTextHelpFormatter) command_map = { 'help': 'help', 'version': 'version', - 'load-overlay': load_overlay, 'save-state': save_state, 'toggle-states': toggle_states, 'autosave-state': autosave_state, diff --git a/blend.hook b/blend.hook index d38921b..3be4e3a 100644 --- a/blend.hook +++ b/blend.hook @@ -17,14 +17,14 @@ run_latehook() { mkdir -p /new_root/blend/overlay/current/usr/bin \ /new_root/blend/overlay/current/usr/sbin \ - /new_root/blend/overlay/current/usr/share/plymouth + /new_root/blend/overlay/current/usr/share mkdir -p /new_root/usr/bin \ /new_root/usr/sbin \ - /new_root/usr/share/plymouth + /new_root/usr/share rm -rf /new_root/blend/overlay/workdir_1 /new_root/blend/overlay/workdir_2 /new_root/blend/overlay/workdir_3 mkdir -p /new_root/blend/overlay/workdir_1 /new_root/blend/overlay/workdir_2 /new_root/blend/overlay/workdir_3 mount -t overlay overlay -o 'lowerdir=/new_root/usr/bin,upperdir=/new_root/blend/overlay/current/usr/bin,workdir=/new_root/blend/overlay/workdir_1' /new_root/usr/bin -o index=off mount -t overlay overlay -o 'lowerdir=/new_root/usr/sbin,upperdir=/new_root/blend/overlay/current/usr/sbin,workdir=/new_root/blend/overlay/workdir_2' /new_root/usr/sbin -o index=off - mount -t overlay overlay -o 'lowerdir=/new_root/usr/share/plymouth,upperdir=/new_root/blend/overlay/current/usr/share/plymouth,workdir=/new_root/blend/overlay/workdir_3' /new_root/usr/share/plymouth -o index=off + mount -t overlay overlay -o 'lowerdir=/new_root/usr/share,upperdir=/new_root/blend/overlay/current/usr/share,workdir=/new_root/blend/overlay/workdir_3' /new_root/usr/share -o index=off } diff --git a/init-blend b/init-blend index b610b20..71c2b92 100755 --- a/init-blend +++ b/init-blend @@ -21,6 +21,8 @@ if [ ! -f '/run/.containerenv' ]; then exit 1 fi +shopt -s extglob + while true; do case $1 in --uid) @@ -74,6 +76,11 @@ cat << 'EOF' ░ ░ ░ ░ ░ ░ ░ ░ ░ +=================== + Credits +=================== + +* NVIDIA driver support - Luca Di Maio (from Distrobox) EOF echo @@ -85,7 +92,7 @@ bmount() { ! [[ -e "$2" ]] && findmnt "$2" &>/dev/null && umount "$2" # unmount target dir if a mount [[ -d "$1" ]] && mkdir -p "$2" # create target dir if source is a dir - [[ -f "$1" ]] && touch "$2" # create target file if source is a file + [[ -f "$1" ]] && mkdir -p "$(dirname "$2")"; touch "$2" # create target file if source is a file mountflags="rslave" @@ -104,10 +111,12 @@ if command -v apt-get &>/dev/null; then diffutils findutils gnupg2 sudo time util-linux libnss-myhostname \ libvte-2.9[0-9]-common libvte-common lsof ncurses-base passwd \ pinentry-curses libegl1-mesa libgl1-mesa-glx libvulkan1 mesa-vulkan-drivers &>/dev/null + elif command -v pacman &>/dev/null; then pacman --noconfirm -Syyu &>/dev/null pacman --noconfirm -Sy bash bc curl wget diffutils findutils gnupg sudo time util-linux vte-common lsof ncurses pinentry \ - mesa opengl-driver vulkan-intel vulkan-radeon &>/dev/null + mesa opengl-driver vulkan-intel vulkan-radeon base-devel git &>/dev/null + elif command -v dnf &>/dev/null; then dnf install -y --allowerasing bash bc curl wget diffutils findutils dnf-plugins-core gnupg2 less lsof passwd pinentry \ procps-ng vte-profile ncurses util-linux sudo time shadow-utils vulkan mesa-vulkan-drivers \ @@ -173,6 +182,62 @@ bmount "/usr/bin/host-blend" "/usr/bin/blend" ro if [[ ! -f '/.init_blend.lock' ]]; then +####################################################################### + +# NVIDIA driver integration. This is straight from https://github.com/89luca89/distrobox/blob/main/distrobox-init#L816, +# entirely thanks to an effort by Luca Di Maio, save for a few tweaks for init-blend. Thanks, in case you're reading this! + +NVIDIA_FILES="$(find /run/host/usr/ \ + -path "/run/host/usr/share/doc*" -prune -o \ + -path "/run/host/usr/src*" -prune -o \ + -path "/run/host/usr/lib*/modules*" -prune -o \ + -path "/run/host/usr/share/man*" -prune -o \ + -path "/run/host/usr/lib*" -prune -o \ + -type f -iname "*nvidia*" -print 2/dev/null +chown root /etc/sudo.conf +chown root /usr/bin/sudo +chmod 4755 /usr/bin/sudo + fi -touch /.init_blend.lock +if [[ ! -f '/.init_blend.lock' ]] && command -v pacman &>/dev/null; then + cd /; git clone https://aur.archlinux.org/yay.git &>/dev/null; cd yay + chown -R "$_uname" . &>/log + sudo -u "$_uname" makepkg --noconfirm -si &>/dev/null + cd /; rm -rf yay + + touch /.init_blend.lock +fi echo echo "Completed container setup." From f7cc861115230258e55b15a4626af47c1065ad9f Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Mon, 17 Apr 2023 12:57:47 +0530 Subject: [PATCH 036/121] Fix a typo in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6e9ea10..823f357 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This repository also contains **blend-settings**, a tool for configuring blend a ## Credits -The `init-blend` file in this repository uses a few lines (the sections have been clearly) uses from distrobox's init script. These lines have been marked and attributed appropriately, and are licensed under [the GPL-3.0 license](https://github.com/89luca89/distrobox/blob/main/COPYING.md). +The `init-blend` file in this repository uses a few lines (the sections have been marked clearly) uses from distrobox's init script. These lines have been marked and attributed appropriately, and are licensed under [the GPL-3.0 license](https://github.com/89luca89/distrobox/blob/main/COPYING.md). I would also like to thank Luca Di Maio from Distrobox for NVIDIA driver support in containers. From 5756f61ec169b4d42576ba43166ee30727c1ff1b Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Mon, 17 Apr 2023 16:48:56 +0530 Subject: [PATCH 037/121] Clear binaries from removed containers --- blend-files | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/blend-files b/blend-files index 08e7bf5..69e6298 100755 --- a/blend-files +++ b/blend-files @@ -4,8 +4,8 @@ import os import sys import yaml import time +import glob import getpass -import shutil import fileinput import subprocess @@ -105,8 +105,6 @@ def create_container_binaries(): for c, i in _binaries: try: - if i == 'apt': - print(c, i) os.symlink(os.path.expanduser( f'~/.local/bin/blend_{c}/{i}'), os.path.expanduser(f'~/.local/bin/blend_bin/{i}')) except FileExistsError: @@ -126,6 +124,10 @@ def create_container_binaries(): os.remove(os.path.join(os.path.expanduser( f'~/.local/bin/blend_bin'), b)) + for b_dir in glob.glob(os.path.expanduser(f'~/.local/bin/blend_*')): + if os.path.basename(b_dir) != 'blend_bin' and os.path.basename(b_dir)[6:] not in _list: + subprocess.call(['rm', '-rf', b_dir], shell=False) + def create_container_applications(): _apps = [] From a86d8ff96608e8ac6e6ef37d0e0833eaba3ad91f Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Mon, 17 Apr 2023 21:09:16 +0530 Subject: [PATCH 038/121] remove Electron development menu from blend-settings --- blend-settings/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blend-settings/main.js b/blend-settings/main.js index fe99cbc..6f721e8 100644 --- a/blend-settings/main.js +++ b/blend-settings/main.js @@ -24,7 +24,7 @@ function createWindow() { autoHideMenuBar: true }) - //mainWindow.setMenu(null) + mainWindow.setMenu(null) mainWindow.loadFile('src/index.html') } From 80c4de06377a2e019dc26a3e7bb28f59a21d0f9d Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Tue, 18 Apr 2023 00:15:09 +0530 Subject: [PATCH 039/121] Reduce the gap between blend-files checks to 1 second --- blend-files | 2 +- blend-settings/src/internal/js/containers.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/blend-files b/blend-files index 69e6298..f194596 100755 --- a/blend-files +++ b/blend-files @@ -233,4 +233,4 @@ while True: create_container_binaries() create_container_applications() - time.sleep(15) + time.sleep(1) diff --git a/blend-settings/src/internal/js/containers.js b/blend-settings/src/internal/js/containers.js index 2fdb870..07dc151 100644 --- a/blend-settings/src/internal/js/containers.js +++ b/blend-settings/src/internal/js/containers.js @@ -16,7 +16,8 @@ function create_container () { 'cmd': `blend create-container -cn ${container_name} -d ${container_distro} \ && echo 'created container successfully (exiting automatically in 5 seconds)' \ || echo 'container creation failed (exiting automatically in 5 seconds)'; - sleep 5` }); + sleep 5` }) + $('#inputContainerName').val('') ipc.on('container-created', () => { worker.postMessage('update-list') }) From e54dbe03e35076cf5cbfba6e591d326473a271b6 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Tue, 18 Apr 2023 12:54:03 +0530 Subject: [PATCH 040/121] Fix Android app store buttons --- blend-settings/src/internal/js/android.js | 28 +++++++++++++++-------- blend-settings/src/pages/android.html | 2 +- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/blend-settings/src/internal/js/android.js b/blend-settings/src/internal/js/android.js index b6088a2..20dafd7 100644 --- a/blend-settings/src/internal/js/android.js +++ b/blend-settings/src/internal/js/android.js @@ -48,15 +48,15 @@ function install_aurora_store() { aurora_store_worker.onmessage = e => { if (e.data == 'success') { document.getElementById('aurora-store-btn').outerHTML = - `` + `` } else if (e.data == 'failed') { document.getElementById('aurora-store-btn').outerHTML = `` + class="btn btn-success" disabled>Failed` setTimeout(() => { document.getElementById('aurora-store-btn').outerHTML = - `` }, 2000) } @@ -85,15 +85,15 @@ function install_f_droid() { f_droid_worker.onmessage = e => { if (e.data == 'success') { document.getElementById('f-droid-btn').outerHTML = - `` + `` } else if (e.data == 'failed') { document.getElementById('f-droid-btn').outerHTML = `` setTimeout(() => { document.getElementById('f-droid-btn').outerHTML = - `` }, 2000) } @@ -114,14 +114,22 @@ require('fs').stat('/var/lib/waydroid', (err, stat) => { require('child_process').spawn('waydroid', ['session', 'start']) setTimeout(() => { if (require('child_process').spawnSync('waydroid', ['app', 'list']).stdout.includes('com.aurora.store')) { + document.getElementById('aurora-store-btn').outerHTML = + `` + } else { document.getElementById('aurora-store-btn').outerHTML = `` + class="btn btn-success">Install` } if (require('child_process').spawnSync('waydroid', ['app', 'list']).stdout.includes('org.fdroid.fdroid')) { document.getElementById('f-droid-btn').outerHTML = - `` + `` + } else { + document.getElementById('f-droid-btn').outerHTML = + `` } }, 1000) } else { diff --git a/blend-settings/src/pages/android.html b/blend-settings/src/pages/android.html index e039ff3..ed98b92 100644 --- a/blend-settings/src/pages/android.html +++ b/blend-settings/src/pages/android.html @@ -59,7 +59,7 @@
-

Useful information From 1a18d5fbb345a7a2662323c6b82443bbf9e99049 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Tue, 18 Apr 2023 15:07:07 +0530 Subject: [PATCH 041/121] Add message recommending users to reboot before using Android apps --- blend-settings/src/internal/js/android.js | 1 + blend-settings/src/pages/android.html | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/blend-settings/src/internal/js/android.js b/blend-settings/src/internal/js/android.js index 20dafd7..a7e4e30 100644 --- a/blend-settings/src/internal/js/android.js +++ b/blend-settings/src/internal/js/android.js @@ -22,6 +22,7 @@ function init_waydroid() { if (e.data == 'success') { document.getElementById('waydroid-initialize-settings').classList.add('d-none') document.getElementById('waydroid-initialized-settings').classList.remove('d-none') + document.getElementById('first-time-waydroid').classList.remove('d-none') } } } diff --git a/blend-settings/src/pages/android.html b/blend-settings/src/pages/android.html index ed98b92..5f86b88 100644 --- a/blend-settings/src/pages/android.html +++ b/blend-settings/src/pages/android.html @@ -23,8 +23,9 @@
-

Android app support (WayDroid) has successfully been initialized. Since you're using an NVIDIA GPU, apps will use software +

Android app support (WayDroid) has successfully been initialized. It's + recomended to reboot before using or installing Android apps/stores. Since you're using an NVIDIA GPU, apps will use software rendering.

Install a store
@@ -37,8 +38,8 @@
- +
@@ -51,19 +52,20 @@
- +
- +

Useful information -

In the event that an app you regularly use on your phone is broken on blendOS, you could install MicroG from F-Droid by following the instructions here.

+

In the event that an app you regularly use on your phone is broken on blendOS, you could install MicroG from + F-Droid by following the instructions here.

From 86c1dbdaa49d2e71675cdf646090d05fe7468eff Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Thu, 20 Apr 2023 11:34:27 +0530 Subject: [PATCH 042/121] update packaging --- PKGBUILD | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 6af26cf..99acf09 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -2,8 +2,8 @@ pkgbase=blend-git pkgname=('blend-git' 'blend-settings-git') -pkgver=r27.0024d66 -pkgrel=1 +pkgver=r30.f7cc861 +pkgrel=2 _electronversion=22 pkgdesc="A package manager for blendOS" arch=('x86_64' 'i686') @@ -40,7 +40,7 @@ build() { } package_blend-git() { - depends=('bash' 'blend-settings' 'podman' 'python' 'python-pexpect') + depends=('bash' 'blend-settings-git' 'podman' 'python' 'python-pexpect') provides=("${pkgname%-git}") conflicts=("${pkgname%-git}") @@ -65,6 +65,8 @@ package_blend-git() { package_blend-settings-git() { pkgdesc="blendOS Settings" depends=("electron${_electronversion}") + provides=(blend-settings) + conflicts=(blend-settings) cd "${srcdir}/${pkgbase%-git}/${pkgbase%-git}-settings" From ba1300ef12256aeafb3468f6e5f0aa55d4bb23a5 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Thu, 20 Apr 2023 16:03:48 +0530 Subject: [PATCH 043/121] Add profile script --- PKGBUILD | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 99acf09..25e33d9 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -2,8 +2,8 @@ pkgbase=blend-git pkgname=('blend-git' 'blend-settings-git') -pkgver=r30.f7cc861 -pkgrel=2 +pkgver=r35.1a18d5f +pkgrel=1 _electronversion=22 pkgdesc="A package manager for blendOS" arch=('x86_64' 'i686') @@ -12,10 +12,11 @@ license=('GPL3') makedepends=("electron${_electronversion}" 'git' 'npm') source=('git+https://github.com/blend-os/blend.git' 'blend-settings.desktop' - 'blend-settings') + 'blend-settings' + 'blend.sh') sha256sums=('SKIP' 'a605d24d2fa7384b45a94105143db216db1ffc0bdfc7f6eec758ef2026e61e54' - '73cb7c39190d36f233b8dfbc3e3e6737d56e61e90881ad95f09e5ae1f9b405a8') + '73cb7c39190d36f233b8dfbc3e3e6737d56e61e90881ad95f09e5ae1f9b405a8' 'SKIP') pkgver() { cd "${srcdir}/${pkgbase%-git}" @@ -52,6 +53,8 @@ package_blend-git() { "${pkgname%-git}-system" \ "${pkgname%-git}-files" \ -t "${pkgdir}"/usr/bin/ + install -Dm644 ../"${pkgname%-git}.sh" -t \ + "${pkgdir}"/etc/profile.d/ install -Dm644 "${pkgname%-git}-system.service" -t \ "${pkgdir}"/usr/lib/systemd/system/ install -Dm644 "${pkgname%-git}-files.service" -t \ @@ -93,5 +96,6 @@ package_blend-settings-git() { install -Dm644 "${srcdir}/${pkgname%-git}.desktop" -t \ "${pkgdir}"/usr/share/applications/ + install -Dm755 "${srcdir}/${pkgname%-git}" -t "${pkgdir}"/usr/bin/ } From dbb31c3e87b8d8b93e4caa40a4babdb55ec00386 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Thu, 20 Apr 2023 16:44:18 +0530 Subject: [PATCH 044/121] update gitignore --- .gitignore | 1 + blend.sh | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 blend.sh diff --git a/.gitignore b/.gitignore index 8fee7f7..c565390 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ !.gitignore !PKGBUILD +!blend.sh !blend-settings !blend-settings.desktop !.SRCINFO diff --git a/blend.sh b/blend.sh new file mode 100644 index 0000000..2d57023 --- /dev/null +++ b/blend.sh @@ -0,0 +1,13 @@ +# https://unix.stackexchange.com/a/217629 + +pathmunge () { + if ! echo "$PATH" | /bin/grep -Eq "(^|:)$1($|:)" ; then + if [ "$2" = "after" ] ; then + PATH="$PATH:$1" + else + PATH="$1:$PATH" + fi + fi +} + +pathmunge "${HOME}/.local/bin/blend_bin" From dc7557c3e4669f308c727af99fd936d15fa2eee7 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 21 Apr 2023 12:22:19 +0530 Subject: [PATCH 045/121] Use lspci for NVIDIA detection --- blend-settings/src/internal/js/android.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blend-settings/src/internal/js/android.js b/blend-settings/src/internal/js/android.js index a7e4e30..c49e1d3 100644 --- a/blend-settings/src/internal/js/android.js +++ b/blend-settings/src/internal/js/android.js @@ -9,7 +9,7 @@ function init_waydroid() { setTimeout(() => { require('child_process').spawnSync('pkexec', ['waydroid', 'shell', 'pm', 'disable', 'com.android.inputmethod.latin']) require('child_process').spawnSync('waydroid', ['prop', 'set', 'persist.waydroid.multi_windows', 'true']) - if (require('child_process').spawnSync('sh', ['-c', 'LC_ALL=C glxinfo | grep "^OpenGL renderer string: "']).stdout.includes('NVIDIA')) { + if (require('child_process').spawnSync('sh', ['-c', 'LC_ALL=C lspci']).stdout.includes('NVIDIA')) { require('child_process').spawnSync('sh', ['-c', 'echo "ro.hardware.gralloc=default" | pkexec tee -a /var/lib/waydroid/waydroid.cfg']) require('child_process').spawnSync('sh', ['-c', 'echo "ro.hardware.egl=swiftshader" | pkexec tee -a /var/lib/waydroid/waydroid.cfg']) } @@ -109,7 +109,7 @@ require('fs').stat('/var/lib/waydroid', (err, stat) => { if (err == null) { document.getElementById('waydroid-initialize-settings').classList.add('d-none') document.getElementById('waydroid-initialized-settings').classList.remove('d-none') - if (require('child_process').spawnSync('sh', ['-c', 'LC_ALL=C glxinfo | grep "^OpenGL renderer string: "']).stdout.includes('NVIDIA')) { + if (require('child_process').spawnSync('sh', ['-c', 'LC_ALL=C lspci']).stdout.includes('NVIDIA')) { document.getElementById('nvidia-warning-installed').classList.remove('d-none') } require('child_process').spawn('waydroid', ['session', 'start']) @@ -134,7 +134,7 @@ require('fs').stat('/var/lib/waydroid', (err, stat) => { } }, 1000) } else { - if (require('child_process').spawnSync('sh', ['-c', 'LC_ALL=C glxinfo | grep "^OpenGL renderer string: "']).stdout.includes('NVIDIA')) { + if (require('child_process').spawnSync('sh', ['-c', 'LC_ALL=C lspci']).stdout.includes('NVIDIA')) { document.getElementById('nvidia-warning').classList.remove('d-none') } } From 60acc7ba0501567294956dfc264585d1d343bda1 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 21 Apr 2023 12:50:59 +0530 Subject: [PATCH 046/121] Use lspci for NVIDIA detection --- blend-settings/src/internal/js/android.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/blend-settings/src/internal/js/android.js b/blend-settings/src/internal/js/android.js index c49e1d3..e5590ca 100644 --- a/blend-settings/src/internal/js/android.js +++ b/blend-settings/src/internal/js/android.js @@ -7,14 +7,13 @@ function init_waydroid() { require('child_process').spawnSync('pkexec', ['systemctl', 'enable', '--now', 'waydroid-container']) require('child_process').spawn('sh', ['-c', 'waydroid session start & disown']) setTimeout(() => { - require('child_process').spawnSync('pkexec', ['waydroid', 'shell', 'pm', 'disable', 'com.android.inputmethod.latin']) require('child_process').spawnSync('waydroid', ['prop', 'set', 'persist.waydroid.multi_windows', 'true']) - if (require('child_process').spawnSync('sh', ['-c', 'LC_ALL=C lspci']).stdout.includes('NVIDIA')) { + if (require('child_process').spawnSync('sh', ['-c', 'LC_ALL=C glxinfo | grep "^OpenGL renderer string: "']).stdout.includes('NVIDIA')) { require('child_process').spawnSync('sh', ['-c', 'echo "ro.hardware.gralloc=default" | pkexec tee -a /var/lib/waydroid/waydroid.cfg']) require('child_process').spawnSync('sh', ['-c', 'echo "ro.hardware.egl=swiftshader" | pkexec tee -a /var/lib/waydroid/waydroid.cfg']) } require('child_process').spawn('sh', ['-c', 'pkexec waydroid upgrade -o; waydroid session stop; waydroid session start']) - setTimeout(() => { postMessage('success') }, 1000) + setTimeout(() => { require('child_process').spawnSync('pkexec', ['waydroid', 'shell', 'pm', 'disable', 'com.android.inputmethod.latin']); postMessage('success') }, 5000) }, 2000) ` ) From 38e93d12187f947ea0b53c9f80922d21023063d4 Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 21 Apr 2023 19:13:54 +0530 Subject: [PATCH 047/121] Add timeout to con_get_output --- blend-files | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/blend-files b/blend-files index f194596..aecc6b6 100755 --- a/blend-files +++ b/blend-files @@ -212,8 +212,12 @@ def create_container_sessions(type='xsessions'): f'{session_dir}/blend-{c};{i}'), 0o775) -def con_get_output(name, cmd): return subprocess.run(['sudo', '-u', user, 'podman', 'exec', '--user', getpass.getuser(), '-it', name, 'bash', '-c', cmd], - stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.decode('UTF-8').strip() +def con_get_output(name, cmd): + try: + return subprocess.run(['sudo', '-u', user, 'podman', 'exec', '--user', getpass.getuser(), '-it', name, 'bash', '-c', cmd], + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout=5).stdout.decode('UTF-8').strip() + except subprocess.TimeoutExpired: + return '' user = getpass.getuser() From 87467b6105b5937f189264bc170822e38b762a9c Mon Sep 17 00:00:00 2001 From: Rudra Saraswat Date: Fri, 21 Apr 2023 20:03:28 +0530 Subject: [PATCH 048/121] Add toggle for app grouping --- blend-settings/src/internal/js/system.js | 54 +++++++++++++++++++++++- blend-settings/src/pages/system.html | 15 +++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/blend-settings/src/internal/js/system.js b/blend-settings/src/internal/js/system.js index 99b2cd2..e65b9ca 100644 --- a/blend-settings/src/internal/js/system.js +++ b/blend-settings/src/internal/js/system.js @@ -92,6 +92,13 @@ function check_state_creation() { } } +function check_app_grouping() { + if (require('fs').existsSync(`${require('os').homedir()}/.config/categorize_apps_gnome_disable`)) { + document.getElementById('app-grouping-toggle').setAttribute('checked', '') + } +} + +check_app_grouping() check_state_creation() check_rollback() @@ -133,4 +140,49 @@ $('#automatic-state-toggle').on('change', () => { } } } -}); \ No newline at end of file +}); + +$('#app-grouping-toggle').on('change', () => { + if (!document.getElementById('app-grouping-toggle').checked) { + let enable_autogrouping_worker = new Worker( + `data:text/javascript, + let s = require('child_process').spawnSync('rm', ['-f', '${require('os').homedir()}/.config/categorize_apps_gnome_disable']).status + if (s === 0) { + postMessage('success') + } else { + postMessage('failure') + } + ` + ) + enable_autogrouping_worker.onmessage = e => { + if (e.data == 'success') { + document.getElementById('app-grouping-toggle').checked = false + } else { + document.getElementById('app-grouping-toggle').checked = true + } + } + } else { + let disable_autogrouping_worker = new Worker( + `data:text/javascript, + require('child_process').spawnSync('mkdir', ['-p', '${require('os').homedir()}/.config']).status + let s = require('child_process').spawnSync('touch', ['${require('os').homedir()}/.config/categorize_apps_gnome_disable']).status + if (s === 0) { + postMessage('success') + } else { + postMessage('failure') + } + ` + ) + disable_autogrouping_worker.onmessage = e => { + if (e.data == 'success') { + document.getElementById('app-grouping-toggle').checked = true + } else { + document.getElementById('app-grouping-toggle').checked = false + } + } + } +}); + +if (require('process').env.XDG_CURRENT_DESKTOP.includes('GNOME')) { + $('#app-grouping-item').removeClass('d-none') +} \ No newline at end of file diff --git a/blend-settings/src/pages/system.html b/blend-settings/src/pages/system.html index 800a0fa..9835988 100644 --- a/blend-settings/src/pages/system.html +++ b/blend-settings/src/pages/system.html @@ -45,6 +45,21 @@