Contributing to ProxMenux
ProxMenux is open to community contributions — new scripts, fixes for existing ones, dialog improvements, translations, integrations. This page is the formal door: how the repository is structured, the branching model and pull-request workflow, the script header with author attribution and optional sponsor link, and the design conventions every contribution must follow.
Two related pages, one project
Branching model
ProxMenux uses a three-tier branching model:
| Branch | Purpose |
|---|---|
main | Stable, production-ready code. Only release-grade merges land here. This is what end users get when they install ProxMenux normally. |
develop | Active development branch. All new work lands here first; it's the “beta” channel. Stable releases are cut from here into main when a release is ready. |
feature/* | Short-lived branches per feature, fix or improvement. Created from develop, merged back into develop via a Pull Request after review. |
What this means for users
main get tested releases. Users who want the latest features early — and don't mind occasional rough edges — can follow develop. New features always pass through develop first; nothing reaches main without going through that cycle.Pull request workflow
From idea to merged release in five steps:
- Create a feature branch from
develop:Use a descriptive branch name:git clone https://github.com/MacRimi/ProxMenux.git cd ProxMenux git checkout develop git pull origin develop git checkout -b feature/your-feature-namefeature/zfs-arc-tuning,feature/fix-iommu-detection,feature/add-tailscale-script. - Work on your changes and push the branch:
# Make your changes, then: git add scripts/... git commit -m "Add ZFS ARC tuning script" git push -u origin feature/your-feature-name - Open a Pull Request against
develop(not againstmain). On the GitHub PR creation page, double-check the base branch is set todevelop. Include in the description: what the script does, what it changes, and which Proxmox VE versions you tested it on. - After review, changes are merged into
develop. A maintainer reviews for code quality, header conventions, and the two-phase UI policy. They may request changes — push more commits to the same branch and the PR updates automatically. - Stable releases are merged from
developintomain. When the maintainers cut a release,developgets fast-forwarded intomainand tagged. Your contribution becomes part of the next stable version.
Script header — metadata & description
Every script in ProxMenux opens with two adjacent comment blocks that together form the header. They are both required — together they let any reader know who wrote the script and what it does, all without opening the code itself.
- Top block — metadata. Author, optional GitHub / Sponsor links, maintainer, copyright, license, version, last-updated date. This is also where contributor recognition happens: when you write a new script, your name goes here, and you can optionally include a link to your personal page (GitHub) and a sponsor profile (Ko-fi, GitHub Sponsors, Buy Me a Coffee, etc.).
- Bottom block — description. A short paragraph in plain English explaining what the script does. This is what users read before opening the code — it must be self-contained enough that someone who only sees the header understands the purpose of the script. List the main actions, the resources affected, any prerequisites.
The license line is fixed — GPL-3.0
License line in the header is always the GPL-3.0 reference shown in the example below — it's not a per-script choice. By contributing a script you agree to release it under GPL-3.0, which means anyone can read it, modify it and redistribute it (including modifications) as long as they keep it under the same license. The full text lives at MacRimi/ProxMenux/LICENSE.#!/bin/bash
# ==========================================================
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : Your Name
# GitHub : github.com/yourhandle
# Sponsor : ko-fi.com/yourhandle
# Maintainer : MacRimi
# Copyright : (c) 2026 MacRimi & contributors
# License : (GPL-3.0) (https://github.com/MacRimi/ProxMenux/blob/main/LICENSE)
# Version : 1.0
# Last Updated: DD/MM/YYYY
# ==========================================================
# Description:
# Short paragraph explaining what the script does.
# Mention the main actions (e.g. "creates a ZFS pool",
# "configures IOMMU and reboots", "imports an ISO into a VM"),
# the resources it touches, and any prerequisites the user
# should be aware of before running it.
# ==========================================================The GitHub and Sponsor lines are optional — leave them out if you don't want to publish them. Everything else is required.
Why this matters
Project structure
Where each kind of script lives in the repository:
scripts/
├── menus/ # Top-level menu scripts (entry points)
├── storage/ # Disk, storage and passthrough scripts
├── share/ # NFS, Samba, local share scripts
├── vm/ # VM creation and configuration scripts
├── gpu_tpu/ # GPU / TPU passthrough scripts
├── post_install/ # Post-install automation scripts
├── backup_restore/ # Backup and restore scripts
├── utilities/ # System utility scripts
├── global/ # Shared helper libraries (sourced by other scripts)
├── utils.sh # Shared utility functions and message helpers
└── help_info_menu.sh # Interactive help and command referenceEvery script sources utils.sh at startup to get the message functions, the spinner, color variables and the translation system. Shared helper libraries (in scripts/global/) are sourced explicitly by the scripts that need them.
The two-phase UI design policy
This is the most important convention in ProxMenux and the one PRs are most often asked to fix. Every script is divided into exactly two phases:
| Phase | Purpose | Screen state |
|---|---|---|
| Phase 1 — Selection | Collect every user decision. Run any preparatory work needed (probes, scans, checks). | dialog overlays. If a probe between two menus takes more than a second or two, show a msg_info spinner so the user knows the script hasn't frozen, then call stop_spinner right before the next dialog. |
| Phase 2 — Execution | Execute every operation. Display the full progress history accumulating on screen. | Visible msg_info / msg_ok messages; never dialog. |
The principle: collect everything first, then execute everything. The user sees a clean dialog-driven menu (Phase 1), then a clean log-style execution view (Phase 2). No mixing — no dialog appearing mid-execution, no progress noise during selection.
Phase 1 — typical pattern
# Silent preparatory work between dialogs
msg_info "$(translate "Checking disk assignments...")"
ASSIGNED_TO=$(check_assignments "$DISK")
stop_spinner # ← clears line silently, result saved in variable
# Next dialog can now use ASSIGNED_TO
if [ -n "$ASSIGNED_TO" ]; then
dialog --yesno "$(translate "Disk already assigned. Continue?")" $UI_YESNO_H $UI_YESNO_W
fi
# Collect multiple decisions per item with parallel arrays
declare -a DISK_LIST=()
declare -a DISK_FORMAT_TYPES=()
declare -a DISK_MOUNT_POINTS=()
for DISK in $SELECTED; do
msg_info "$(translate "Analyzing disk...")"
CURRENT_FS=$(lsblk -no FSTYPE "$DISK" | xargs)
stop_spinner
FORMAT=$(dialog --backtitle "$BACKTITLE" \
--title "$(translate "Select Filesystem")" \
--menu "..." $UI_SHORT_MENU_H $UI_SHORT_MENU_W $UI_SHORT_MENU_LIST_H \
"ext4" "..." "xfs" "..." "btrfs" "..." \
2>&1 >/dev/tty)
[ -z "$FORMAT" ] && continue
DISK_LIST+=("$DISK")
DISK_FORMAT_TYPES+=("$FORMAT")
doneRules for Phase 1:
- If a
msg_infospinner is currently running and you need to open adialogorwhiptailmenu, callstop_spinnerfirst — the spinner can't coexist with the overlay drawn by either tool. If no spinner is active, you don't need to call it. - Use
show_proxmenux_logo+msg_title+msg_infowhen you need to give the user visual context for a long-running operation in Phase 1 (e.g. a probe that takes 5+ seconds). The function includes a screen clear, so don't callclearbefore it. - Don't call
show_proxmenux_logobetween dialog menus where there's nothing to display — clearing the screen for an empty terminal is just visual noise. - Store all decisions and probe results in variables or parallel arrays. The visible recap happens at the start of Phase 2, not in Phase 1.
Phase 2 — typical pattern
# ── PHASE 2 — EXECUTION ─────────────────────────────
show_proxmenux_logo
msg_title "$(translate "My Script Title")"
# Recap Phase 1 preparatory results — show what was already done
msg_ok "$(translate "CT $CTID selected.")"
msg_ok "$(translate "Repositories verified.")"
msg_ok "$(translate "Disks to process: ${#DISK_LIST[@]}")"
# Now execute operations
for i in "${!DISK_LIST[@]}"; do
DISK="${DISK_LIST[$i]}"
FORMAT="${DISK_FORMAT_TYPES[$i]}"
msg_info "$(translate "Formatting") $DISK $(translate "as") $FORMAT..."
mkfs."$FORMAT" "$DISK" >/dev/null 2>&1
msg_ok "$(translate "Formatted.")"
done
msg_ok "$(translate "Completed. ${#DISK_LIST[@]} disk(s) processed.")"
msg_success "$(translate "Press Enter to return to menu...")"
read -rRules for Phase 2: always start with show_proxmenux_logo + msg_title; immediately recap Phase 1 results as msg_ok lines; never call show_proxmenux_logo again (it would clear accumulated progress); never call dialog in Phase 2 — if a runtime decision is truly unavoidable, use whiptail (see next section).
When to use dialog vs whiptail
Both tools draw text-mode user interfaces, but they behave very differently on screen — and ProxMenux uses them in two distinct phases for a reason.
| Tool | When to use it | Effect on screen |
|---|---|---|
dialog | Always in Phase 1. Every interactive selection — picking a VM, a disk, a filesystem, confirming an action — uses dialog. This is the default UI tool of ProxMenux. | Takes over the terminal: clears the screen, draws its overlay, and on close returns the terminal to its prior state. Fine in Phase 1 because nothing useful is showing yet. |
whiptail | Only in Phase 2, only when unavoidable. The typical case is a reboot prompt at the end of execution. Don't reach for whiptail to ask “continue?” mid-execution — that decision should have been made in Phase 1. | Lighter-weight overlay that does not clear the terminal context. The accumulated msg_ok / msg_info history of Phase 2 stays visible behind the dialog box. That's why it's the right choice when progress is already on screen. |
Why the split matters
dialog in the middle of Phase 2, every msg_ok line the user has been watching disappears. You wipe the audit trail. whiptail avoids that. So the rule isn't arbitrary — it's about preserving the user's view of what the script has done so far.Reboot prompt — the canonical Phase 2 whiptail
When a script ends and a reboot may be required (e.g. IOMMU enabled, kernel parameters changed), the prompt at the end of Phase 2 uses whiptail. Always include a “No” branch that warns the user not to use the affected resource until they reboot:
if [[ "$HOST_REBOOT_REQUIRED" == "yes" ]]; then
echo ""
if whiptail --title "$(translate "Reboot Required")" --yesno \
"\n$(translate "A host reboot is required before starting the VM. Reboot now?")" \
13 78; then
msg_warn "$(translate "Rebooting the system...")"
reboot
else
echo ""
msg_info2 "$(translate "Do not start the VM until the system has been rebooted.")"
fi
fi
msg_success "$(translate "Press Enter to return to menu...")"
read -rMessage functions reference
All defined in utils.sh. Use them as the default for any user-visible output — consistent visuals across scripts is the whole point. If your script needs a new function that doesn't fit the existing set (a new severity level, a new layout helper, etc.), propose it in your Pull Request — it'll be reviewed and added to utils.sh if it's broadly useful.
| Function | When to use | Spinner |
|---|---|---|
| msg_info "text" | Operation in progress. | Starts |
| stop_spinner | End of silent preparatory work in Phase 1 — kills spinner, clears the line. | Stops |
| msg_ok "text" | Operation succeeded. Also use for “feature enabled” even when a reboot is required. | Stops |
| msg_warn "text" | Actual warning or degraded state. | Stops |
| msg_error "text" | Fatal error. | Stops |
| msg_info2 "text" | Non-blocking advisory (cyan info line). | Stops |
| msg_success "text" | Final “Press Enter to return” prompt at the end of Phase 2. | Stops |
| msg_title "text" | Bold title with built-in spacing. Used at the start of Phase 2. | — |
| show_proxmenux_logo | Clears screen, shows the logo. Called once at the start of Phase 2 only. | — |
dialog conventions
- Always pass
--backtitle "$BACKTITLE"to everydialogandwhiptailcall.$BACKTITLEis always"ProxMenux"— set once at the script header and never overridden. The user must always see the project name as the framing context, never the script's own title. - Always wrap titles and messages with
$(translate "..."). - Always redirect
dialogoutput with2>&1 >/dev/tty— the captured stdout becomes the user's selection, while the dialog itself draws on the terminal. - Use the standard UI dimension variables (
$UI_MENU_H,$UI_MSG_W, etc.) for consistent sizing across scripts. - Always check for empty / cancelled selections and handle them gracefully.
Complete example — building a VM-selection menu and handling cancellation:
# 1) Build the list of VMs as alternating "ID NAME" pairs.
# dialog --menu expects this exact shape: tag1 description1 tag2 description2 ...
VM_LIST=""
for vmid in $(qm list | awk 'NR>1 {print $1}'); do
vm_name=$(qm config "$vmid" | awk -F': ' '/^name:/ {print $2}')
VM_LIST="$VM_LIST $vmid \"$vm_name\""
done
# 2) Show the dialog. Captured stdout is the user's selection (the VMID).
VMID=$(eval "dialog --backtitle \"\$BACKTITLE\" \
--title \"\$(translate \"Select VM\")\" \
--menu \"\$(translate \"Choose a VM from the list:\")\" \
\$UI_MENU_H \$UI_MENU_W \$UI_MENU_LIST_H \
$VM_LIST \
2>&1 >/dev/tty")
# 3) Empty result means the user pressed Cancel or Esc — exit silently.
if [ -z "$VMID" ]; then
exit 0
fi
# 4) Continue with the rest of Phase 1, knowing VMID is now set.
echo "User selected VMID=$VMID"Translation policy
All user-visible strings must be wrapped with the translate function. ProxMenux translates them automatically into all supported languages — you write English, the user reads their native language.
msg_ok "$(translate "Operation completed successfully.")"
msg_error "$(translate "Failed to start container") $CTID."
dialog --title "$(translate "Select Storage")" ...- Write strings in English — translation is handled automatically by the build.
- Keep strings concise. Avoid embedding variables inside long sentences where possible.
- Do not translate variable names, paths or technical identifiers.
Variable & style conventions
- Use
UPPER_CASEfor script-level variables. - Use
lower_casefor local function variables (declare withlocal). - Quote all variable expansions:
"$VAR"— not$VAR. - Use
[[ ]]for conditionals, not[ ](except where POSIX is required). show_proxmenux_logois the appropriate way to clear the screen — it includes the clear and shows the project logo so the user always has visual context. Call it once at the start of Phase 2 (and optionally before a long Phase 1 spinner block).
Standard variable names across the project:
CTID # container ID
VMID # virtual machine ID
DISK # device path (e.g. /dev/sdb)
PARTITION # partition path (e.g. /dev/sdb1)
STORAGE # Proxmox storage name
MOUNT_POINT # filesystem mount pathRedirecting tool output during Phase 2
Phase 2 displays a clean log of msg_info → msg_ok lines accumulating on screen. If a tool you call (apt, mkfs, qm, pct, dd, etc.) writes its own output to stdout/stderr, it scrolls past your messages and breaks the visual flow — you end up with a chaotic terminal where the user can't tell the script's own progress lines from the underlying tool's noise.
Without redirect — what the user sees if you don't handle the noise:
ℹ Installing kernel package...
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following NEW packages will be installed:
proxmox-kernel-6.5
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
Need to get 12.5 MB of archives.
After this operation, 47.2 MB of additional disk space will be used.
Get:1 http://download.proxmox.com/debian/pve trixie/pve-no-subscription amd64 ...
... [200+ more lines of dpkg output] ...
✓ Kernel installed.With redirect — what the same operation looks like when noise is sent elsewhere:
ℹ Installing kernel package...
✓ Kernel installed.
ℹ Configuring kernel command line...
✓ Configured.
ℹ Refreshing boot loader...
✓ Boot loader updated.Two patterns to choose from:
- Discard the output when you don't need it — fastest, simplest:
DEBIAN_FRONTEND=noninteractive apt-get install -y "$package" >/dev/null 2>&1 - Send the output to a log file when you may want to inspect it later (debugging a failed install, checking what dpkg actually did). This is the preferred pattern for any apt operation:
apt-get install -y "$package" >> "$log_file" 2>&1
The script scripts/global/update-pve9_2.sh is a reference implementation — every apt-get call sends output to a log file so the user only sees the clean msg_info → msg_ok flow, while the log on disk lets you reconstruct exactly what apt did if anything goes wrong.
Do's and Don'ts
✅ Do
stop_spinnerbefore adialogin Phase 1 only when amsg_infospinner is currently running.- Phase 2 starts with
show_proxmenux_logo + msg_title + msg_okrecap of Phase 1 results. - Use
msg_okfor successfully enabled features — even if a reboot is required. - Use
whiptail(notdialog) for any post-execution prompt that must appear in Phase 2. - Always include a “No” branch in reboot dialogs that warns the user not to start the affected resource until rebooted.
- Guard VM-only logic by checking
[[ -f "/etc/pve/qemu-server/${vmid}.conf" ]]— controllers and NVMe PCIe can't be added to LXC containers. - Use
ensure_repositoriesfromutils-install-functions.shinstead of unconditionalapt-get update. - Use parallel arrays in Phase 1 when each item needs multiple dialogs.
❌ Don't
- Call
dialogwhile a spinner is active. - Skip the Phase 1 recap at the start of Phase 2.
- Call
show_proxmenux_logoa second time — it erases everything Phase 2 has printed. - Use
dialogin Phase 2 (usewhiptailfor the rare unavoidable case). - Use bare
clear. - Wrap
msg_titleinecho ""blank lines — it already includes spacing. - Use
msg_warnto report a successfully enabled feature — that's anmsg_ok. - Run unconditional
apt-get update— useensure_repositories.
Submitting your contribution
- Fork
MacRimi/ProxMenuxon GitHub and clone your fork. - Create a
feature/*branch fromdevelop(see Branching model above). - Follow this guide for any new or modified scripts. Write the header with your name; add the optional sponsor line if you want.
- Test your script on a real Proxmox VE instance — both Phase 1 (every dialog branch) and Phase 2 (every operation succeeds and rolls back cleanly on error).
- Open a Pull Request against
developwith a clear description: what the script does, what changed, which Proxmox VE version it was tested on. - Make sure your contribution respects the Code of Conduct.
For security-sensitive issues, follow the disclosure flow in SECURITY.md rather than opening a public issue.
Where to next
- CONTRIBUTING.md (full guide) — the source of truth for every convention, including the advanced hardware patterns.
- Contributors — the people whose work has shaped ProxMenux releases. (Different page from this one.)
- Code of Conduct — the standards every contributor agrees to follow.
- GitHub Discussions — ask before you build if you're not sure whether an idea fits, or to find collaborators on a larger feature.