Package Superwhisper with upstream beta tracking (#854)

* Package Superwhisper with upstream beta tracking

* Seed fresh Superwhisper shortcuts before daemon startup

* Refresh checksum for Superwhisper user setup

* Fix Superwhisper panel setup and packaging review findings

* Read shell replies when retrying Superwhisper panel setup

* Exclude Superwhisper portable menu integration
This commit is contained in:
David Heinemeier Hansson authored and GitHub committed 2026-10-08 20:29:07 +02:00
1 parent e285432795
commit 7d2c7c50af
11 files changed
+422

No files matched your search

+1
View File
@@ -67,6 +67,7 @@ jobs:
./tests/artifact-helpers.sh
./tests/limine-mkinitcpio-hook.sh
./tests/voxtype-bin-install.sh
./tests/superwhisper-bin-install.sh
pacman -S --noconfirm --quiet rclone >/dev/null
./tests/publish-artifact.sh
'
@@ -0,0 +1,16 @@
{
"source": "local",
"release_ring": "fast",
"min_release_age": "24h",
"upstream": {
"watch": {
"github": "superultrainc/superwhisper-linux-releases",
"allow_prerelease": true,
"pattern": "v(?P<version>[0-9]+(?:\\.[0-9]+)*)-beta\\.(?P<beta>[0-9]+)",
"version": "{version}beta{beta}",
"variables": {
"_version": "{version}-beta.{beta}"
}
}
}
}
+87
View File
@@ -0,0 +1,87 @@
# Maintainer: Omarchy <https://omarchy.org>
pkgname=superwhisper-bin
pkgver=0.1.0beta15
_version=0.1.0-beta.15
pkgrel=1
pkgdesc='Local and cloud voice dictation with Superwhisper'
arch=('x86_64')
url='https://superwhisper.com'
license=('LicenseRef-proprietary' 'MIT')
provides=('superwhisper')
conflicts=('superwhisper')
depends=('glibc' 'libgcc' 'libstdc++' 'libgomp' 'xdg-utils' 'python' 'python-gobject' 'python-cairo' 'gtk4'
'gtk4-layer-shell' 'at-spi2-core' 'pipewire-pulse' 'libnotify'
'wl-clipboard' 'ffmpeg' 'vulkan-icd-loader' 'librsvg' 'fontconfig'
'hicolor-icon-theme' 'foot')
optdepends=('fcitx5: insert dictation directly through the input method'
'vulkan-intel: local processing on Intel GPUs'
'vulkan-radeon: local processing on AMD GPUs')
makedepends=('minisign')
options=('!debug' '!strip')
_release="https://tux.superwhisper.com/x86_64/v$_version/superwhisper-omarchy-x86_64-$_version.tar.zst"
source=("superwhisper-$pkgver.tar.zst::$_release"
"superwhisper-$pkgver.tar.zst.minisig::$_release.minisig"
'linux-release.pub'
'superwhisper'
'superwhisper.service'
'superwhisper.desktop'
'superwhisper-url.desktop'
'setup-user')
noextract=("superwhisper-$pkgver.tar.zst")
sha256sums=('99e44cbeb60c44945d2713144f12d0a356b1dfb98ed5702b5468fb6b47ae49c0'
'4d933540a9299f5f7835d1f6494ce3facad866c477cdf0e82bf3e9ae5813325d'
'0672b5230ed6390f9168eaea009c6ce1e8838f0961a87888d9073d1e1db5267d'
'54a0fd574f2b123af24031e910fe0dc47be1d4c055ee5df775f1c681e6201c93'
'8b9a2cb81a6ea015d663c9981ba67edafe934eb0e56904fab5030fbbfdd8506b'
'c65169c8432223f26b072c8defe9ff65ef82c26be34cd92a9617ed112cb22570'
'e67978d239e12a386ec335346b14e10cc7e3bed8b9b1d4bcbed4377f10d4695a'
'ffbf2f42e403fca85eb73810742397884fbf949b715ed33cb98f618f33556c16')
prepare() {
minisign -Vm "superwhisper-$pkgver.tar.zst" -x "superwhisper-$pkgver.tar.zst.minisig" -p linux-release.pub
bsdtar -xf "superwhisper-$pkgver.tar.zst"
local bundle="superwhisper-omarchy-x86_64-$_version"
[[ $(< "$bundle/VERSION") == "$_version" ]] || return 1
# Qt 6.12 adds a Color type that shadows Omarchy's palette singleton.
python - "superwhisper-omarchy-x86_64-$_version/app/assets/omarchy-plugin/superwhisper-panel" <<'PYTHON'
from pathlib import Path
import re
import sys
for path in Path(sys.argv[1]).glob("*.qml"):
original = path.read_text()
updated = re.sub(r"(?<![\w.])Color\b", "Commons.Color", original)
if updated != original:
alias = r"(?m)^import qs\.Commons(?: [0-9.]+)? as Commons[ \t]*$"
if not re.search(alias, updated):
updated, imports = re.subn(r"(?m)^(import qs\.Commons(?: [0-9.]+)?[ \t]*)$", r"\1\nimport qs.Commons as Commons", updated, count=1)
if imports != 1:
raise SystemExit(f"Cannot qualify palette import in {path}")
if len(re.findall(alias, updated)) != 1 or re.search(r"(?<![\w.])Color\b", updated):
raise SystemExit(f"Invalid palette references in {path}")
path.write_text(updated)
PYTHON
}
package() {
local bundle="superwhisper-omarchy-x86_64-$_version"
install -dm755 "$pkgdir/opt/superwhisper"
cp -a --no-preserve=ownership "$bundle/app/." "$pkgdir/opt/superwhisper/"
# Omarchy owns its Dictation menu; omit the portable installer's menu writer.
rm -f "$pkgdir/opt/superwhisper/scripts/omarchy-menu-rows.py"
install -Dm755 superwhisper "$pkgdir/usr/bin/superwhisper"
install -Dm644 superwhisper.service "$pkgdir/usr/lib/systemd/user/superwhisper.service"
install -Dm644 superwhisper.desktop "$pkgdir/usr/share/applications/superwhisper.desktop"
install -Dm644 superwhisper-url.desktop "$pkgdir/usr/share/applications/superwhisper-url.desktop"
install -Dm755 setup-user "$pkgdir/usr/share/superwhisper/setup-user"
install -Dm644 -t "$pkgdir/usr/share/licenses/$pkgname/" "$bundle"/licenses/*
install -Dm644 "$bundle/README.md" "$pkgdir/usr/share/doc/$pkgname/README.md"
install -Dm644 "$bundle/app/assets/linux-status/SuperwhisperTUI.ttf" "$pkgdir/usr/share/fonts/superwhisper/SuperwhisperTUI.ttf"
for size in 16 32 64 128 256 512 1024; do
install -Dm644 "$bundle/app/assets/app-icon/$size.png" "$pkgdir/usr/share/icons/hicolor/${size}x${size}/apps/superwhisper.png"
done
install -dm755 "$pkgdir/usr/share/fcitx5/addon"
sed 's|@LIBRARY@|/opt/superwhisper/lib/fcitx5/libsuperwhisper-fcitx5|' \
"$bundle/app/assets/fcitx5/superwhisper.conf.in" > "$pkgdir/usr/share/fcitx5/addon/superwhisper.conf"
}
+15
View File
@@ -0,0 +1,15 @@
# Superwhisper
Packages the vendor's signed portable x86_64 Linux archive under `/opt/superwhisper`, with a launcher, desktop entries, user service, fonts, icons and the fcitx5 text-commit addon. Local models are downloaded through Superwhisper's settings; they are not bundled in the package.
`sync-upstream` follows beta releases from `superultrainc/superwhisper-linux-releases`, normalizing `0.1.0-beta.15` to the Arch version `0.1.0beta15` while retaining the vendor version in `_version`. The stable feed has not been published yet. The recipe verifies the archive's SHA256 and Minisign signature against the checked-in vendor release key. Packaging never runs the downloaded installer.
Omarchy's Setup → Defaults → Dictation → Superwhisper invokes `/usr/share/superwhisper/setup-user`, then configures its shortcuts and saves Superwhisper as the default backend. The package does not add Omarchy menu extensions and omits the portable installer’s menu-writing helper. User setup enables the packaged service, links the vendor's panel and agent skill, and retires known portable-install wrappers and unit overrides with backups. Preferences, history and downloaded models are retained. Package updates come from OPR; the portable install's private update timer is disabled.
Omarchy loads the selected backend's generated `~/.config/superwhisper/shortcuts.lua` bridge itself, so no Superwhisper block is required in the user's Hyprland files. The bridge retains cancellation and correction-based vocabulary learning alongside Omarchy's shared dictation commands.
The launcher redirects the vendor’s generated Hyprland include to a private, unloaded file for both the daemon and CLI. User setup waits for daemon readiness. The portable updater is disabled and refused by the packaged launcher. The archive is verified before extraction, its version is checked, and the bundled panel uses explicit Omarchy palette references for Qt 6.12.
Fresh profiles start with Alt+Space toggle, no native hold, and Escape cancellation before the daemon starts, avoiding a conflict with Omarchy’s Right Alt binding. Existing preferences remain unchanged.
Panel discovery is asynchronous: user setup retries placement, leaves a pending marker if the shell is unavailable, and retries on the next setup without moving an already placed panel. Bundled component license notices are installed under `/usr/share/licenses/superwhisper-bin/`.
@@ -0,0 +1,2 @@
untrusted comment: minisign public key BC524820AE37B966
RWRmuTeuIEhSvCYQejUR+zzMNsfxAn7pr9iOZ2ZJ9U1/zJwfsqvhzATB
+99
View File
@@ -0,0 +1,99 @@
#!/bin/bash
set -euo pipefail
config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
backup_suffix=".before-opr.$(date +%s%N)"
# Retire the portable install's wrappers and unit overrides. Keep its versioned
# binaries, preferences, history and models so the change is reversible.
launcher="$HOME/.local/bin/superwhisper"
mkdir -p "$HOME/.local/bin"
if [[ -f $launcher && ! -L $launcher ]] && grep -Fq 'superwhisper/app/current/run.sh' "$launcher"; then
mv "$launcher" "$launcher$backup_suffix"
ln -s /usr/bin/superwhisper "$launcher"
fi
if [[ ! -e $launcher && ! -L $launcher ]]; then
ln -s /usr/bin/superwhisper "$launcher"
fi
current="$data_home/superwhisper/app/current"
if [[ -L $current && $(readlink "$current") != "/opt/superwhisper" ]]; then
mv "$current" "$current$backup_suffix"
ln -s /opt/superwhisper "$current"
elif [[ ! -e $current && ! -L $current ]]; then
mkdir -p "${current%/*}"
ln -s /opt/superwhisper "$current"
fi
unit="$config_home/systemd/user/superwhisper.service"
if [[ -f $unit ]] && grep -Fq 'ExecStart=%h/.local/bin/superwhisper daemon' "$unit"; then
mv "$unit" "$unit$backup_suffix"
fi
systemctl --user disable --now superwhisper-update.timer 2>/dev/null || true
# Seed a fresh profile before the daemon starts. Its upstream Right Alt hold
# conflicts with Omarchy's own bindings and prevents sequential CLI changes.
preferences="$config_home/superwhisper/preferences.json"
if [[ ! -e $preferences ]]; then
mkdir -p "${preferences%/*}"
(
umask 077
printf '%s\n' '{"toggleRecordingShortcut":"Alt+Space","pushToTalkShortcut":"","cancelRecordingShortcut":"Escape"}' > "$preferences"
)
fi
systemctl --user daemon-reload
systemctl --user enable --now superwhisper.service
systemctl --user restart superwhisper.service
ready=false
for (( attempt = 0; attempt < 50; attempt++ )); do
if superwhisper status >/dev/null 2>&1; then
ready=true
break
fi
sleep 0.1
done
if [[ $ready != "true" ]]; then
echo "Superwhisper did not become ready. Check systemctl --user status superwhisper.service." >&2
exit 1
fi
xdg-mime default superwhisper-url.desktop x-scheme-handler/superwhisper
# Link the vendor's panel and agent guide to the packaged payload. They update
# with pacman; no copy of the application or private updater is installed.
plugins="$config_home/omarchy/plugins"
panel_pending="$config_home/superwhisper/omarchy-panel.pending"
if [[ ! -e $plugins/superwhisper-panel && ! -L $plugins/superwhisper-panel ]]; then
touch "$panel_pending"
fi
mkdir -p "$plugins" "$HOME/.agents/skills"
ln -sfn /opt/superwhisper/assets/omarchy-plugin/superwhisper-panel "$plugins/superwhisper-panel"
ln -sfn /opt/superwhisper/assets/agent-skill/superwhisper "$HOME/.agents/skills/superwhisper"
for agent in .claude .codex .pi/agent; do
if [[ -d $HOME/$agent ]]; then
mkdir -p "$HOME/$agent/skills"
ln -sfn /opt/superwhisper/assets/agent-skill/superwhisper "$HOME/$agent/skills/superwhisper"
fi
done
omarchy-shell -q shell rescanPlugins
if [[ -f $panel_pending ]]; then
for (( attempt = 0; attempt < 50; attempt++ )); do
placement=$(omarchy-shell shell putBarWidget superwhisper.panel '{"section":"right"}' 2>/dev/null) || placement=""
if [[ $placement == "ok" ]]; then
rm "$panel_pending"
break
elif [[ -z $placement ]]; then
break
fi
sleep 0.1
done
if [[ -f $panel_pending ]]; then
echo "Superwhisper is ready; panel setup will retry next time setup-user runs." >&2
fi
fi
# A running input method discovers the packaged text-commit addon on restart.
if systemctl --user -q is-active omarchy-fcitx5.service; then
systemctl --user restart omarchy-fcitx5.service
fi
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
# Omarchy loads shortcuts.lua from its selected backend, so the vendor's
# generated Hyprland block must live in a file Hyprland does not load.
export SUPERWHISPER_HYPR_BINDINGS="${XDG_CONFIG_HOME:-$HOME/.config}/superwhisper/hypr-block.lua"
mkdir -p "${SUPERWHISPER_HYPR_BINDINGS%/*}"
if [[ ! -e $SUPERWHISPER_HYPR_BINDINGS ]]; then
install -m600 /dev/null "$SUPERWHISPER_HYPR_BINDINGS"
fi
if [[ ${1:-} == "update" ]]; then
echo "Superwhisper is updated through Omarchy's package repository." >&2
exit 1
fi
exec /opt/superwhisper/run.sh "$@"
@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=Superwhisper License Activation
Exec=superwhisper open-url %u
Icon=superwhisper
Terminal=false
NoDisplay=true
MimeType=x-scheme-handler/superwhisper;
@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=Superwhisper
Comment=Local and cloud voice dictation
Exec=foot --app-id=com.superwhisper.settings --title=Superwhisper --font="JetBrainsMono Nerd Font,Superwhisper TUI:size=12" env SW_TUI_LOGO=font superwhisper settings
Icon=superwhisper
Terminal=false
Categories=Utility;Accessibility;
@@ -0,0 +1,17 @@
[Unit]
Description=Superwhisper dictation
After=graphical-session.target pipewire.service
PartOf=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/bin/superwhisper daemon
ExecStopPost=/usr/bin/superwhisper playback-restore
Restart=on-failure
RestartSec=2
KillMode=control-group
TimeoutStopSec=15
UMask=0077
[Install]
WantedBy=graphical-session.target
+154
View File
@@ -0,0 +1,154 @@
#!/bin/bash
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
export HOME="$test_tmp/home" XDG_CONFIG_HOME="$test_tmp/config" XDG_DATA_HOME="$test_tmp/data"
export SUPERWHISPER_TEST_LOG="$test_tmp/calls"
mkdir -p "$test_tmp/bin" "$HOME/.local/bin" "$XDG_CONFIG_HOME/systemd/user" "$XDG_DATA_HOME/superwhisper/app/old"
export PATH="$test_tmp/bin:$PATH"
for command in systemctl xdg-mime superwhisper; do
cat > "$test_tmp/bin/$command" <<'SH'
#!/bin/bash
printf '%s %s\n' "${0##*/}" "$*" >> "$SUPERWHISPER_TEST_LOG"
[[ $* != *is-active* ]]
SH
done
cat > "$test_tmp/bin/omarchy-shell" <<'SH'
#!/bin/bash
printf '%s %s\n' "${0##*/}" "$*" >> "$SUPERWHISPER_TEST_LOG"
[[ $1 != "-q" ]] || exit 0
[[ ${SUPERWHISPER_SHELL_ABSENT:-0} != 1 ]] || exit 1
if [[ $* == *putBarWidget* ]]; then
count=0
[[ ! -f $SUPERWHISPER_PANEL_COUNT ]] || read -r count < "$SUPERWHISPER_PANEL_COUNT"
printf '%s\n' "$((count + 1))" > "$SUPERWHISPER_PANEL_COUNT"
if (( count < 2 )); then
echo 'not ready'
else
echo ok
fi
fi
SH
export SUPERWHISPER_PANEL_COUNT="$test_tmp/panel-count"
chmod +x "$test_tmp/bin/"*
cat > "$HOME/.local/bin/superwhisper" <<'SH'
#!/bin/bash
exec "${XDG_DATA_HOME:-$HOME/.local/share}/superwhisper/app/current/run.sh" "$@"
SH
printf '%s\n' 'ExecStart=%h/.local/bin/superwhisper daemon' > "$XDG_CONFIG_HOME/systemd/user/superwhisper.service"
ln -s "$XDG_DATA_HOME/superwhisper/app/old" "$XDG_DATA_HOME/superwhisper/app/current"
printf '%s\n' 'retained model' > "$XDG_DATA_HOME/superwhisper/app/old/model"
mkdir -p "$XDG_CONFIG_HOME/superwhisper"
printf '%s\n' 'retained preferences' > "$XDG_CONFIG_HOME/superwhisper/preferences.json"
bash "$ROOT/pkgbuilds/superwhisper-bin/setup-user"
[[ $(readlink "$HOME/.local/bin/superwhisper") == "/usr/bin/superwhisper" ]]
[[ $(readlink "$XDG_DATA_HOME/superwhisper/app/current") == "/opt/superwhisper" ]]
[[ ! -e $XDG_CONFIG_HOME/systemd/user/superwhisper.service ]]
compgen -G "$XDG_CONFIG_HOME/systemd/user/superwhisper.service.before-opr.*" >/dev/null
compgen -G "$HOME/.local/bin/superwhisper.before-opr.*" >/dev/null
[[ $(cat "$XDG_DATA_HOME/superwhisper/app/old/model") == "retained model" ]]
[[ $(cat "$XDG_CONFIG_HOME/superwhisper/preferences.json") == "retained preferences" ]]
grep -Fxq 'systemctl --user disable --now superwhisper-update.timer' "$SUPERWHISPER_TEST_LOG"
grep -Fxq 'systemctl --user restart superwhisper.service' "$SUPERWHISPER_TEST_LOG"
[[ $(readlink "$XDG_CONFIG_HOME/omarchy/plugins/superwhisper-panel") == "/opt/superwhisper/assets/omarchy-plugin/superwhisper-panel" ]]
[[ $(readlink "$HOME/.agents/skills/superwhisper") == "/opt/superwhisper/assets/agent-skill/superwhisper" ]]
bash "$ROOT/pkgbuilds/superwhisper-bin/setup-user"
(( $(grep -c '^omarchy-shell shell putBarWidget ' "$SUPERWHISPER_TEST_LOG") == 3 ))
[[ ! -e $XDG_CONFIG_HOME/superwhisper/omarchy-panel.pending ]]
printf '%s\n' 'PASS: Superwhisper setup migrates portable wrappers with backups, preserves data, and keeps the panel placement'
# A fresh install supplies the user launcher and current link the vendor panel
# expects, without installing a private updater or touching Hyprland config.
export HOME="$test_tmp/fresh" XDG_CONFIG_HOME="$test_tmp/fresh-config" XDG_DATA_HOME="$test_tmp/fresh-data"
bash "$ROOT/pkgbuilds/superwhisper-bin/setup-user"
[[ $(readlink "$HOME/.local/bin/superwhisper") == "/usr/bin/superwhisper" ]]
[[ $(readlink "$XDG_DATA_HOME/superwhisper/app/current") == "/opt/superwhisper" ]]
[[ ! -e $XDG_CONFIG_HOME/hypr ]]
python3 - "$XDG_CONFIG_HOME/superwhisper/preferences.json" <<'PY_TEST'
import json
import os
import sys
with open(sys.argv[1]) as profile:
preferences = json.load(profile)
assert preferences["toggleRecordingShortcut"] == "Alt+Space"
assert preferences["pushToTalkShortcut"] == ""
assert preferences["cancelRecordingShortcut"] == "Escape"
assert os.stat(sys.argv[1]).st_mode & 0o777 == 0o600
PY_TEST
printf '%s\n' 'PASS: fresh Superwhisper setup uses the package and needs no personal Hyprland config'
# Missing shell cannot fail backend setup; placement retries next time.
export HOME="$test_tmp/deferred" XDG_CONFIG_HOME="$test_tmp/deferred-config" XDG_DATA_HOME="$test_tmp/deferred-data"
SUPERWHISPER_SHELL_ABSENT=1 bash "$ROOT/pkgbuilds/superwhisper-bin/setup-user" 2> "$test_tmp/panel-warning"
[[ -f $XDG_CONFIG_HOME/superwhisper/omarchy-panel.pending ]]
bash "$ROOT/pkgbuilds/superwhisper-bin/setup-user"
[[ ! -e $XDG_CONFIG_HOME/superwhisper/omarchy-panel.pending ]]
printf '%s\n' 'PASS: asynchronous panel discovery retries and unavailable shell does not abort setup'
# The packaged launcher must put the native include in a file Hyprland never
# loads, for the daemon as well as clients. Model the verified vendor contract.
cat > "$test_tmp/vendor-run" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >> "$SUPERWHISPER_TEST_LOG"
printf '%s\n' '-- Generated native include' > "$SUPERWHISPER_HYPR_BINDINGS"
SH
chmod +x "$test_tmp/vendor-run"
sed "s|/opt/superwhisper/run.sh|$test_tmp/vendor-run|" "$ROOT/pkgbuilds/superwhisper-bin/superwhisper" > "$test_tmp/launcher"
mkdir -p "$XDG_CONFIG_HOME/hypr"
printf '%s\n' 'personal bindings' > "$XDG_CONFIG_HOME/hypr/bindings.lua"
for action in daemon 'shortcuts apply' 'shortcuts set hold none'; do
bash "$test_tmp/launcher" $action
done
[[ $(cat "$XDG_CONFIG_HOME/hypr/bindings.lua") == "personal bindings" ]]
[[ $(cat "$XDG_CONFIG_HOME/superwhisper/hypr-block.lua") == "-- Generated native include" ]]
if bash "$test_tmp/launcher" update apply; then
echo 'FAIL: the portable updater must not replace a packaged installation' >&2
exit 1
fi
printf '%s\n' 'PASS: packaged daemon and clients redirect native includes and refuse the private updater'
# A daemon that never becomes ready must stop user setup with a useful error.
cat > "$test_tmp/bin/superwhisper" <<'SH'
#!/bin/bash
exit 1
SH
cat > "$test_tmp/bin/sleep" <<'SH'
#!/bin/bash
exit 0
SH
chmod +x "$test_tmp/bin/superwhisper" "$test_tmp/bin/sleep"
if bash "$ROOT/pkgbuilds/superwhisper-bin/setup-user" 2> "$test_tmp/startup-error"; then
echo 'FAIL: user setup must report a daemon that failed to start' >&2
exit 1
fi
grep -Fq 'Superwhisper did not become ready' "$test_tmp/startup-error"
printf '%s\n' 'PASS: user setup fails clearly when the daemon never becomes ready'
# Exercise the actual packaging rewrite with vendor import variations.
python3 - "$ROOT/pkgbuilds/superwhisper-bin/PKGBUILD" "$test_tmp/palette" <<'PY_TEST'
from pathlib import Path
import subprocess
import sys
recipe = Path(sys.argv[1]).read_text()
patch = recipe.split("<<'PYTHON'\n", 1)[1].split("\nPYTHON\n", 1)[0]
folder = Path(sys.argv[2])
folder.mkdir()
qml = folder / "Panel.qml"
for imports in ("import qs.Commons", "import qs.Commons 1.0", "import qs.Commons\nimport qs.Commons as Commons"):
qml.write_text(imports + "\nItem { property color ink: Color.foreground }\n")
subprocess.run([sys.executable, "-", str(folder)], input=patch, text=True, check=True)
result = qml.read_text()
assert result.count("import qs.Commons as Commons") == 1
assert "Commons.Color.foreground" in result
for imports in ("import QtQuick", "import qs.Commons\nimport qs.Commons as Commons\nimport qs.Commons as Commons"):
qml.write_text(imports + "\nItem { property color ink: Color.foreground }\n")
result = subprocess.run([sys.executable, "-", str(folder)], input=patch, text=True, capture_output=True)
assert result.returncode != 0, "invalid imports must fail packaging"
PY_TEST
printf '%s\n' 'PASS: palette rewrite handles existing/versioned imports and fails on missing or duplicate aliases'