DistroHopper/dh
Claude 29ce40f05d
Add quickget/qget switch and test suite
qget:
- Add QGET_TEST_MODE guard so the script can be sourced for unit
  testing without executing the main logic

dh:
- Auto-detect getter tool (qget preferred, quickget fallback) in
  set_variables(); export as GETTER
- Add getter_list_os() wrapper using GETTER --list for portable
  OS enumeration
- Replace hard-coded quickget calls with $GETTER in
  renew_supported_vms(), renew_ready_vms(), and execmd strings
- Fix releases/editions/description parsing (sed instead of cut -f2
  which fails on colon-space separators)
- Add DH_TEST_MODE guard to prevent execution when sourcing for tests

action:
- Auto-detect getter tool at startup; export as GETTER
- Add --qget / -q and --quickget / -Q flags to override detection

tests/:
- tests/lib.sh: shared assert helpers (assert_eq, assert_ne,
  assert_match, assert_cmd, assert_fn_exists) with pass/fail counting
- tests/test_dh.sh: 31 unit tests covering set_variables, GETTER,
  getter_list_os, root_check, wayland_check, and all required
  function definitions
- tests/test_qget.sh: 23 unit tests covering os_support,
  test_result formatting, is_valid_language, check_hash, and all
  utility functions
- tests/test_action_files.sh: 784 structural tests verifying every
  action file has OSNAME, PRETTY, HOMEPAGE (valid URL), DESCRIPTION,
  releases_() and get_() functions
- tests/run_tests.sh: top-level runner executing all three suites

https://claude.ai/code/session_01M2UXTtQwzcGCNRnFiP2efQ
2026-02-28 19:42:33 +00:00

380 lines
13 KiB
Bash
Executable file

#!/usr/bin/env bash
function wayland_check() {
session_type=$(loginctl show-session "$XDG_SESSION_ID" -p Type | cut -d= -f2)
case "$session_type" in
wayland)
export GDK_BACKEND=x11
echo "Running on Wayland"
;;
x11)
echo "Running on X"
;;
"")
echo "Running on TTY"
;;
*)
echo "Not sure where I am running"
;;
esac
}
root=""
function root_check() {
if [ "$(id -u)" != 0 ]; then
if [ -f /usr/bin/sudo ]; then
echo "$(tput bold)INFO: Using sudo for root operations.$(tput sgr0)"
root="sudo"
elif [ -f /usr/bin/doas ]; then
echo "$(tput bold)INFO: Using doas for root operations.$(tput sgr0)"
root="doas"
fi
fi
}
function set_variables() {
# DEBUG mod
#bash -x ./dh 2>&1 | tee output.log
#progname="${progname:="${0##*/}"}"
progname="DistroHopper"
version="0.90"
#GTK_THEME="alt-dialog"
DH_CONFIG_DIR="$HOME/.config/dh"
DH_CONFIG="$DH_CONFIG_DIR/config"
DH_ICON_DIR="$DH_CONFIG_DIR/icons"
VMS_DIR="$HOME/vm"
PATH_PREFIX="/usr/bin/"
TMP_DIR="/tmp"
if command -v sakura &>/dev/null; then
terminal="sakura"
elif command -v xterm &>/dev/null; then
terminal="xterm"
elif command -v x-terminal-emulator &>/dev/null; then
terminal="x-terminal-emulator"
else
terminal="xterm"
fi
# Detect getter tool: prefer qget (native), fall back to quickget
if command -v qget &>/dev/null; then
GETTER="qget"
elif command -v quickget &>/dev/null; then
GETTER="quickget"
else
GETTER="quickget"
fi
replace='"!"'
export "DH_CONFIG_DIR" "DH_CONFIG" "replace" "DH_ICON_DIR" "PATH_PREFIX" "TMP_DIR" "terminal" "VMS_DIR" "GETTER"
#check_and_set_mode
# Set traps to catch the signals and exit gracefully
trap "exit" INT
}
# installation ---------------------------------------------------------
function dependencies_check_gui() {
[ -f "$PATH_PREFIX/yad" ] || echo "Yad not installed!"
}
function install_needed_apt() {
$root apt install qemu bash coreutils ovmf grep jq lsb procps python3 genisoimage usbutils util-linux sed spice-client-gtk swtpm wget xdg-user-dirs zsync unzip yad
}
function install_needed_pacman() {
$root pacman -S cdrtools coreutils edk2-ovmf grep jq procps python3 qemu-full sed socat spice-gtk swtpm usbutils util-linux wget xdg-user-dirs xorg-xrandr zsync getext yad
}
function install_needed_xbps() {
$root xbps-install -S qemu bash coreutils grep jq procps-ng python3 util-linux sed spice-gtk swtpm usbutils wget xdg-user-dirs xrandr unzip zsync socat yad
}
function install_needed_dnf() {
$root dnf install qemu bash coreutils edk2-tools grep jq lsb procps python3 genisoimage usbutils util-linux sed spice-gtk-tools swtpm wget xdg-user-dirs xrandr unzip yad
}
function install_dependencies() {
# Find the current distribution and install dependecies
if [ -f /etc/os-release ]; then
if [ -f "$PATH_PREFIX/pacman" ]; then
install_needed_pacman
elif [ -f "$PATH_PREFIX/apt" ]; then
install_needed_apt
elif [ -f "$PATH_PREFIX/xbps-install" ]; then
install_needed_xbps
elif [ -f "$PATH_PREFIX/dnf" ]; then
install_needed_dnf
else
echo "Error unknown distro!"
#exit 1
fi
fi
}
function create_dirs() {
mkdir -p "$DH_CONFIG_DIR"
touch "$DH_CONFIG"
mkdir -p "$DH_ICON_DIR"
}
function choose_vms_dir() {
NEWDIR="$(yad --width=900 --height=900 --file --directory --title="Where to save VMs?")"
VMS_DIR="$NEWDIR"
export VMS_DIR
echo "VMS_DIR=\"$VMS_DIR\"\nexport VMS_DIR" > "$DH_CONFIG"
}
function install_dh() {
cp -f dh quickget quickemu quickreport chunkcheck "$PATH_PREFIX" >/dev/null 2>&1 || $root cp -f dh quickget quickemu quickreport chunkcheck "$PATH_PREFIX"
cd /tmp/
wget https://github.com/oSoWoSo/distro-svg/archive/refs/heads/main.zip
unzip main.zip
cp distro-svg-main/*.svg distro-svg-main/*.png "$DH_ICON_DIR/"
#cp -f icons/* "$DH_ICON_DIR/" >/dev/null 2>&1 || $root cp -f icons/* "$DH_ICON_DIR/"
mkdir -p "$DH_CONFIG_DIR/ready"
mkdir -p "$DH_CONFIG_DIR/supported"
}
# shellcheck disable=SC2154
function create_desktop_entry() {
cat <<EOF >${DESKTOP_FILE}
[Desktop Entry]
Version=$version
Type=$type
Name=$name
GenericName=$gname
Comment=$comment
Exec=$execmd
Icon=$icon
Terminal=$terminal
X-MultipleArgs=$args
Type=$type
Categories=$categories
StartupNotify=$notify
MimeType=$mime
Keywords=$keyword
EOF
}
function desktop_entry_dh() {
echo "$MSG_INTERM"
run_in_terminal
DESKTOP_FILE="${TMP_DIR}/dh.desktop"
type='Application'
name='DistroHopper'
comment='Quickly download, create and run VM of any operating system.'
version="${version}"
execmd="sh -c 'cd ${VMS_DIR} && dh'"
if [ "$interminal" = "yes" ]; then
terminal='true'
fi
icon="$DH_ICON_DIR/hop.svg"
categories='System;Virtualization;'
export -f create_desktop_entry
create_desktop_entry
$root cp "${TMP_DIR}/dh.desktop" /usr/share/applications/
}
function installation_process() {
dependencies_check_gui
install_dependencies
create_dirs
choose_vms_dir
install_dh
desktop_entry_dh
}
# basic ----------------------------------------------------------------
#run_in_terminal() {
# yad --title "terminal" --text "Do you want show output in the terminal?" && interminal="yes" || interminal="no"
#}
function renew_ready_vms() {
cd "$VMS_DIR" || { echo "ERROR: VMS_DIR not set!"; exit 1; }
rm -r "$DH_CONFIG_DIR"/ready >/dev/null 2>&1
mkdir -p "$DH_CONFIG_DIR"/ready
for vm_conf in *.conf; do
echo "creating..."
VMname=$(basename "$VMS_DIR/$vm_conf" .conf)
# Use fuzzy matching to find the best matching icon file (ready to run VMs)
icon_name=$(basename "$VMS_DIR/$vm_conf" .conf | cut -d'-' -f -2)
icon_file=$(find "$DH_ICON_DIR" -type f -iname "${icon_name// /}.*")
# If no icon was found, try shorter name (ready to run VMs)
if [ -z "$icon_file" ]; then
icon_name=$(basename "$VMS_DIR/$vm_conf" .conf | cut -d'-' -f1)
icon_file=$(find "$DH_ICON_DIR" -type f -iname "${icon_name// /}.*")
fi
if [ -z "$icon_file" ]; then
icon_file="$DH_ICON_DIR/tux.svg"
fi
DESKTOP_FILE="${DH_CONFIG_DIR}/ready/${VMname}.desktop"
type='Application'
name="${VMname}"
comment=$("$GETTER" --show "$VMname" 2>/dev/null | grep 'Description:' | sed 's/.*Description: //')
version="${version}"
execmd="sh -c 'cd ${VMS_DIR} && quickemu -vm ${vm_conf};$SHELL'"
icon="${icon_file}"
categories='System;Virtualization;'
create_desktop_entry
done
}
function renew_ready() {
cd "$VMS_DIR" || { echo "ERROR: VMS_DIR not set!"; exit 1; }
# for files in "$VMS_DIR"/*; do
# if [ ! -e *.conf ]; then
# echo $"No .conf files found"
# return
# fi
for vm_conf in *.conf; do
if [ "$vm_conf" == "distrohopper.conf" ]; then
continue # skip processing distrohopper.conf
fi
VMname=$(basename "$VMS_DIR/$vm_conf" .conf)
# Use fuzzy matching to find the best matching icon file (ready to run VMs)
icon_name=$(basename "$VMS_DIR/$vm_conf" .conf | cut -d'-' -f -2)
icon_file=$(find "$DH_ICON_DIR" -type f -iname "${icon_name// /}.*")
# If no icon was found, try shorter name (ready to run VMs)
if [ -z "$icon_file" ]; then
icon_name=$(basename "$VMS_DIR/$vm_conf" .conf | cut -d'-' -f1)
icon_file=$(find "$DH_ICON_DIR" -type f -iname "${icon_name// /}.*")
fi
if [ -z "$icon_file" ]; then
icon_file="$DH_ICON_DIR/tux.svg"
fi
# content of desktop files (ready to run VMs)
mkdir -p "$DH_CONFIG_DIR/ready"
cat <<EOF >"$DH_CONFIG_DIR/ready/$VMname.desktop"
[Desktop Entry]
Type=Application
Name=$VMname
Exec=sh -c 'cd ${VMS_DIR} && quickemu -vm ${vm_conf}'
Icon=$icon_file
Categories=System;Virtualization;
EOF
done
}
function getter_list_os() {
"$GETTER" --list 2>/dev/null | tail -n +2 | awk '{print $1}' | sort -u
}
function renew_supported_vms() {
echo "Regenerating supported"
rm -r "$DH_CONFIG_DIR"/supported >/dev/null 2>&1
mkdir -p "$DH_CONFIG_DIR"/supported
#run_in_terminal
# get supported VMs
getter_list_os >"$DH_CONFIG_DIR/supported.md"
while read -r get_name; do
VMname=$(echo "$get_name" | tr ' ' '_')
releases=$("$GETTER" --show "$get_name" 2>/dev/null | grep 'Releases:' | sed 's/.*Releases: //' | sed 's/ *$//')
editions=$("$GETTER" --show "$get_name" 2>/dev/null | grep 'Editions:' | sed 's/.*Editions: //' | sed 's/ *$//')
icon_name="$DH_ICON_DIR/$get_name"
if [ -f "$icon_name.svg" ]; then
icon_file="$icon_name.svg"
elif [ -f "$icon_name.png" ]; then
icon_file="$icon_name.png"
else
icon_file="$DH_ICON_DIR/tux.svg"
fi
echo "creating $VMname entry"
echo ""
# Check if there are editions
DESKTOP_FILE="$DH_CONFIG_DIR/supported/$VMname.desktop"
type="Application"
name="$get_name"
if [ -z "$editions" ]; then
execmd="sh -c 'cd ${VMS_DIR} && GDK_BACKEND=x11 yad --form --field=\"Release:CB\" \"${releases// /$replace}\" | cut -d\"|\" -f1 | xargs -I{} sh -c \"${GETTER} ${get_name} {}\";$SHELL'"
else
execmd="sh -c 'cd ${VMS_DIR} && GDK_BACKEND=x11 yad --form --separator=\" \" --field=\"Release:CB\" \"${releases// /$replace}\" --field=\"Edition:CB\" \"${editions// /$replace}\" | xargs -I{} sh -c \"${GETTER} ${get_name} {}\";$SHELL'"
fi
if [ "$interminal" = "yes" ]; then
terminal="true"
fi
icon="$icon_file"
categories='System;Virtualization;'
create_desktop_entry
done <"$DH_CONFIG_DIR"/supported.md
}
function distrohopper_about() {
yad --title="About DistroHopper" \
--text="<b>DistroHopper v${version}</b>\n\nQuickly download, create and run VM of any operating system.\n\nBuilt on top of Quickemu.\n\nLicense: MIT\nHomepage: https://github.com/oSoWoSo/DistroHopper" \
--button="OK:0" \
--center --width=400
}
function help_show() {
yad --title="DistroHopper Help" \
--text="<b>DistroHopper v${version} - Help</b>\n\n<b>Tabs:</b>\n \u2022 Run VM - Launch existing virtual machines\n \u2022 Download VM - Download and create new VMs\n \u2022 Options - Configuration and management\n\n<b>Options tab:</b>\n \u2022 Set VMs directory - Change where VMs are stored\n \u2022 Install dh - Install DistroHopper system-wide\n \u2022 Renew supported - Refresh available OS list\n \u2022 Renew ready - Refresh installed VMs list\n\n<b>Usage:</b>\n 1. First run: Set your VMs directory\n 2. Go to 'Download VM' tab and select an OS\n 3. Choose release and edition, then download\n 4. Launched VM appears in 'Run VM' tab" \
--button="OK:0" \
--center --width=500
}
function first_run() {
installation_process
echo "Installation done"
echo "Update supported..."
renew_supported_vms
}
function distrohopper_run_gui() {
key=$((RANDOM % 9000 + 1000))
yad --plug="$key" --tabnum=1 --monitor --icons --listen --read-dir="$DH_CONFIG_DIR"/ready --sort-by-name --no-buttons --borders=0 --icon-size=46 --item-width=76 &
yad --plug="$key" --tabnum=2 --monitor --icons --listen --read-dir="$DH_CONFIG_DIR"/supported --sort-by-name --no-buttons --borders=0 --icon-size=46 --item-width=76 &
yad --dynamic --notebook --key="$key" --monitor --listen --window-icon="$DH_ICON_DIR"/hop.svg --width=900 --height=900 --title="DistroHopper" --tab="run VM" --tab="download VM"
}
function WIP_buttons() {
export root
export VMSDIR
#export -f run_in_terminal
export -f distrohopper_about
export -f help_show
export -f first_run
export -f create_desktop_entry
export -f renew_supported_vms
export -f renew_ready
export -f renew_ready_vms
key=$((RANDOM % 9000 + 1000))
yad --plug="$key" --tabnum=1 --monitor --icons --listen \
--read-dir="$DH_CONFIG_DIR/ready" --sort-by-name --borders=0 \
--icon-size=46 --item-width=76 &
yad --plug="$key" --tabnum=2 --monitor --icons --listen \
--read-dir="$DH_CONFIG_DIR/supported" --sort-by-name --borders=0 \
--icon-size=46 --item-width=76 &
yad --plug="$key" --tabnum=3 --monitor --icons --borders=0 --use-interp \
--icon-size=46 --item-width=76 --columns=1 --form --text-align=center \
--field="Set VMs directory!!Set default directory where VMs are stored":DIR \
--field="Install dh!!Install DistroHopper":FBTN "first_run" \
--field="Renew supported!!Update supported VMs":FBTN "renew_supported_vms" \
--field="Renew ready!!Update ready to run VMs":FBTN "renew_ready_vms" \
--field="Help!!Show this help and exit":FBTN "help_show" \
--field="About!!Show info about DistroHopper":FBTN "distrohopper_about" &
yad --dynamic --notebook --key="$key" --monitor --listen \
--mouse --selectable-labels --no-buttons \
--window-icon="$DH_ICON_DIR/hop.svg" --width=900 --height=900 \
--title="DistroHopper" --tab="run VM" --tab="download VM" --tab="Options"
# posible: --undecorated --fixed ontop --buttons-layout=spread edge start end center --keep-icon-size --image=IMAGE --splash
VAR1="$?"
echo " DEBUG: VAR1 = $VAR1"
echo $?
}
if [ -z "${DH_TEST_MODE}" ]; then
wayland_check
root_check
set_variables
if [ -f "$DH_CONFIG" ]; then
echo "Already installed"
else
first_run
echo "Installation finished"
fi
#distrohopper_run_gui
WIP_buttons
fi