nocoder/install.sh
28allday 749e102bd5 Initial release: NO-CODER batch ProRes transcoder for Omarchy
Native GTK4 + libadwaita app that wraps ffmpeg to batch-convert source
video into editorial-ready Apple ProRes .mov. Targets Omarchy / Hyprland
on Arch Linux specifically.

Highlights:

* Real ffmpeg encode (prores_ks → prores fallback) with live progress
  parsing, cancelable serial queue, disk-space pre-check, source-missing
  guard, output-collision (N) suffixes.
* GPU decode auto-probe at install time — picks cuda → qsv → vaapi based
  on what actually initialises on the host. ProRes encoding stays on CPU
  (no vendor ships a GPU encoder); offloading the decode side cuts wall
  time 25-40% on H.264 / HEVC sources.
* Theme-aware: tracks the active Omarchy theme on every launch by
  parsing colors.toml / ghostty.conf / alacritty.toml / kitty.conf in
  priority order. 34 stock + custom themes verified.
* Pro camera support: .MXF (Canon XF / Sony XDCAM / Panasonic AVC-Intra)
  with proxy-directory pruning so dropping a Sony XAVC card maps masters
  in CLIP/ but skips the low-res duplicates in SUB/.
* Multi-track audio preserved — 4 mono PCM streams from a Canon C300/C500
  land in the output as 4 separate tracks. Optional 24-bit toggle.
* Live encode-speed indicator with ffmpeg -progress parsing; ETA refines
  from real measured throughput rather than a fixed heuristic.
* Hyprland-aware install — registers walker entry, six hicolor icon
  sizes, float+centre windowrule for class dev.nocoder.NoCoder.

Distribution model: git clone + bash install.sh. The installer copies the
source tree to ~/.local/share/nocoder/ so the clone is disposable. Updates
are git pull + re-run install.sh.

Documented at README.md.
2026-04-21 20:43:14 +01:00

328 lines
12 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# install.sh — integrate NO-CODER into Omarchy.
#
# Installs pacman dependencies, drops a launcher into ~/.local/bin, registers
# a .desktop entry so the walker finds it, installs the app icon into the
# hicolor theme, and appends Hyprland windowrules so the window always floats
# centered on launch.
#
# Safe to re-run — the Hyprland rules live inside a marked block that is
# replaced (not duplicated) on every install.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SRC_DIR="$SCRIPT_DIR"
PKG_DIR="$SCRIPT_DIR/packaging"
APP_ID="dev.nocoder.NoCoder"
LAUNCHER_NAME="nocoder"
BIN_DIR="$HOME/.local/bin"
DESKTOP_DIR="$HOME/.local/share/applications"
HICOLOR_DIR="$HOME/.local/share/icons/hicolor"
INSTALL_DIR="$HOME/.local/share/nocoder"
HYPR_CONF="$HOME/.config/hypr/windows.conf"
MARK_BEGIN="# >>> nocoder windowrules begin"
MARK_END="# <<< nocoder windowrules end"
GREEN=$'\e[32m'; YELLOW=$'\e[33m'; RED=$'\e[31m'; DIM=$'\e[2m'; RESET=$'\e[0m'
say() { printf '%s==>%s %s\n' "$GREEN" "$RESET" "$*"; }
warn() { printf '%s[!]%s %s\n' "$YELLOW" "$RESET" "$*" >&2; }
die() { printf '%s[x]%s %s\n' "$RED" "$RESET" "$*" >&2; exit 1; }
# ---------- environment checks ----------
[[ -f "$SRC_DIR/run.py" ]] || die "run.py not found next to install.sh (SRC_DIR=$SRC_DIR)"
[[ -f "$PKG_DIR/$APP_ID.desktop" ]] || die "missing $PKG_DIR/$APP_ID.desktop"
[[ -f "$PKG_DIR/$APP_ID.png" ]] || die "missing $PKG_DIR/$APP_ID.png"
# Guard against running install.sh from inside the install target itself — the
# clean-and-copy step would remove its own script mid-execution.
if [[ "$SRC_DIR" == "$INSTALL_DIR" ]]; then
die "Don't run install.sh from $INSTALL_DIR — run it from your git clone."
fi
if ! command -v pacman >/dev/null 2>&1; then
die "pacman not found — this installer targets Arch/Omarchy only."
fi
if [[ ! -d "$HOME/.local/share/omarchy" ]]; then
warn "$HOME/.local/share/omarchy not found — are you sure this is Omarchy?"
fi
if [[ ! -f "$HOME/.config/hypr/hyprland.conf" ]]; then
die "Hyprland config not found at ~/.config/hypr/hyprland.conf."
fi
# ---------- pacman deps (non-font) ----------
PACMAN_PKGS=(
python
python-gobject
gtk4
libadwaita
ffmpeg
)
# Only invoke sudo/pacman when something is actually missing.
MISSING_PKGS=()
for p in "${PACMAN_PKGS[@]}"; do
pacman -Q "$p" &>/dev/null || MISSING_PKGS+=("$p")
done
if ((${#MISSING_PKGS[@]} == 0)); then
say "All required pacman packages already installed."
else
say "Installing missing pacman packages: ${MISSING_PKGS[*]}"
if command -v omarchy-pkg-add >/dev/null 2>&1; then
omarchy-pkg-add "${MISSING_PKGS[@]}"
else
sudo pacman -S --noconfirm --needed "${MISSING_PKGS[@]}"
fi
fi
# ---------- fonts (per-user, no sudo) ----------
install_font_from_github() {
# $1 friendly name, $2 github repo "owner/name", $3 fc-list match pattern,
# $4 subdir under ~/.local/share/fonts/
local name="$1" repo="$2" fc_pattern="$3" subdir="$4"
# Read fc-list into a var rather than piping to grep -q — with `set -o pipefail`
# grep's early exit gives fc-list a SIGPIPE (141), poisoning the pipeline.
local _fc_all
_fc_all=$(fc-list)
if grep -iqE "$fc_pattern" <<<"$_fc_all"; then
say "$name already available — skipping."
return 0
fi
say "Installing $name to $HOME/.local/share/fonts/$subdir (per-user, no sudo)"
local url
url=$(curl -fsSL "https://api.github.com/repos/$repo/releases/latest" \
| grep -oE '"browser_download_url":[[:space:]]*"[^"]*\.zip"' \
| head -1 | sed -E 's/.*"([^"]*)".*/\1/') || true
if [[ -z "$url" ]]; then
warn "Could not resolve latest $name release — skipping font install."
return 0
fi
local tmpdir
tmpdir=$(mktemp -d)
curl -fsSL -o "$tmpdir/pkg.zip" "$url" || { warn "Download failed: $url"; rm -rf "$tmpdir"; return 0; }
unzip -oq "$tmpdir/pkg.zip" -d "$tmpdir/extract" || { warn "Unzip failed for $name."; rm -rf "$tmpdir"; return 0; }
mkdir -p "$HOME/.local/share/fonts/$subdir"
find "$tmpdir/extract" -type f \( -name "*.otf" -o -name "*.ttf" \) \
-exec cp -f {} "$HOME/.local/share/fonts/$subdir/" \;
rm -rf "$tmpdir"
}
install_font_from_github "Inter" "rsms/inter" '^[^:]*inter[^:]*:' inter
install_font_from_github "JetBrains Mono" "JetBrains/JetBrainsMono" 'jetbrains mono' jetbrains-mono
if command -v fc-cache >/dev/null 2>&1; then
fc-cache -f "$HOME/.local/share/fonts/" >/dev/null 2>&1 || true
fi
# ---------- import smoke test ----------
say "Verifying Python imports"
if ! python3 - <<PY
import sys
sys.path.insert(0, "$SRC_DIR")
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, Gtk # noqa
from nocoder.app import NoCoderApplication # noqa
PY
then
die "Python import check failed. A required module (python-gobject / gtk4 / libadwaita) may not be installed properly."
fi
# ---------- copy source tree into $INSTALL_DIR ----------
# Copy runtime files into a stable location so the user can delete the git
# clone after install. Re-runs wipe the target first to purge files removed
# upstream (e.g., from a git pull) before copying fresh.
#
# Pre-flight: verify every source item exists BEFORE wiping the target. A
# missing item post-wipe would leave the user with no installed app.
for item in run.py style.css nocoder assets; do
[[ -e "$SRC_DIR/$item" ]] || die "missing $SRC_DIR/$item — can't install from an incomplete clone"
done
say "Installing source tree to $INSTALL_DIR"
rm -rf "$INSTALL_DIR"
mkdir -p "$INSTALL_DIR"
cp -r \
"$SRC_DIR/run.py" \
"$SRC_DIR/style.css" \
"$SRC_DIR/nocoder" \
"$SRC_DIR/assets" \
"$INSTALL_DIR/"
# Strip any __pycache__ copied from the source tree — they'd go stale anyway
# and Python will regenerate them as needed.
find "$INSTALL_DIR" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
# ---------- GPU decode probe ----------
# Test which ffmpeg -hwaccel actually initialises on this box (CUDA on NVIDIA,
# QSV on Intel with intel-media-driver, VAAPI on AMD / Intel fallback) and
# pin the result into ~/.config/nocoder/config.json so the app doesn't re-probe
# on every launch. Decode side only — ProRes encode is always CPU.
say "Probing GPU decode"
# A future regression in hwaccel.py would otherwise abort the whole installer
# post-copy — degrade gracefully to CPU decode so the user still ends up with
# a working app they can inspect.
HW_CHOICE="none"
if HW_OUTPUT="$(python3 - <<PY 2>/dev/null
import sys
sys.path.insert(0, "$INSTALL_DIR")
from nocoder.hwaccel import probe_best_hwaccel, save_hwaccel
choice = probe_best_hwaccel()
save_hwaccel(choice)
print(choice or "none")
PY
)"; then
HW_CHOICE="${HW_OUTPUT:-none}"
else
warn "hwaccel probe failed — defaulting to CPU decode. Run the app once to re-probe."
fi
if [[ "$HW_CHOICE" == "none" ]]; then
say " No GPU decode available — decodes will run on CPU."
else
say " Selected: $HW_CHOICE"
fi
# ---------- launcher script in ~/.local/bin ----------
mkdir -p "$BIN_DIR"
LAUNCHER="$BIN_DIR/$LAUNCHER_NAME"
say "Writing launcher to $LAUNCHER"
cat > "$LAUNCHER" <<EOF
#!/usr/bin/env bash
# NO-CODER launcher (installed by install.sh — do not edit by hand).
# Skip the xdg-desktop-portal file chooser so our app's CSS theme applies to
# file dialogs too. Safe on Omarchy — we don't need portal sandboxing.
export GTK_USE_PORTAL=0
exec python3 "$INSTALL_DIR/run.py" "\$@"
EOF
chmod +x "$LAUNCHER"
case ":$PATH:" in
*":$BIN_DIR:"*) ;;
*) warn "$BIN_DIR is not in your PATH — add it to your shell rc for CLI use (the .desktop launcher already uses an absolute path indirectly)." ;;
esac
# ---------- icon ----------
# Drop any previously-installed icons under the old/alternate theme locations,
# so the walker doesn't end up picking a stale version.
rm -f "$HICOLOR_DIR/scalable/apps/$APP_ID.svg"
for sz in 48 64 96 128 256 512; do
rm -f "$HICOLOR_DIR/${sz}x${sz}/apps/$APP_ID.png"
done
# Pick the best downscaler available — ImageMagick (modern "magick" or legacy
# "convert") gives crisp per-size PNGs. Fallback: install source at 256×256
# and let GTK scale on demand.
resize_png() {
local src="$1" dst="$2" size="$3"
if command -v magick >/dev/null 2>&1; then
magick "$src" -resize "${size}x${size}" "$dst"
elif command -v convert >/dev/null 2>&1; then
convert "$src" -resize "${size}x${size}" "$dst"
else
install -m 0644 "$src" "$dst"
fi
}
for sz in 48 64 96 128 256 512; do
dir="$HICOLOR_DIR/${sz}x${sz}/apps"
mkdir -p "$dir"
resize_png "$PKG_DIR/$APP_ID.png" "$dir/$APP_ID.png" "$sz"
done
say "Installed icons under $HICOLOR_DIR/{48,64,96,128,256,512}x*/apps/"
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
# hicolor/ without an index.theme won't regenerate a useful cache — ignore
# the "invalid" report. The PNGs are still discovered by direct lookup.
gtk-update-icon-cache -q -t "$HICOLOR_DIR" >/dev/null 2>&1 || true
fi
# ---------- .desktop file ----------
# The template uses @LAUNCHER@ in Exec= so we can substitute the absolute path
# to the user's launcher. Walker (and systemd-launched GUIs in general) runs
# with a minimal PATH that doesn't include ~/.local/bin, so a bare "Exec=nocoder"
# fails silently from the menu.
mkdir -p "$DESKTOP_DIR"
sed "s|@LAUNCHER@|$LAUNCHER|g" "$PKG_DIR/$APP_ID.desktop" > "$DESKTOP_DIR/$APP_ID.desktop"
chmod 0644 "$DESKTOP_DIR/$APP_ID.desktop"
say "Installed desktop entry to $DESKTOP_DIR/$APP_ID.desktop"
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database -q "$DESKTOP_DIR" || true
fi
if command -v desktop-file-validate >/dev/null 2>&1; then
desktop-file-validate "$DESKTOP_DIR/$APP_ID.desktop" || warn "desktop-file-validate reported warnings."
fi
# ---------- Hyprland windowrules ----------
say "Registering Hyprland windowrules in $HYPR_CONF"
mkdir -p "$(dirname "$HYPR_CONF")"
touch "$HYPR_CONF"
# Strip any previous block (idempotent) — but only if both markers are
# present as a closed pair. An unclosed BEGIN (from a crashed prior run)
# would otherwise cause awk to eat every subsequent line to EOF, including
# hand-edited rules beneath. Leave it alone and warn instead; the user can
# resolve manually, and the fresh block we append below still takes effect.
if grep -qxF "$MARK_BEGIN" "$HYPR_CONF" && ! grep -qxF "$MARK_END" "$HYPR_CONF"; then
warn "found unclosed '$MARK_BEGIN' block in $HYPR_CONF — leaving it intact (remove it manually if stale)."
elif grep -qxF "$MARK_BEGIN" "$HYPR_CONF"; then
tmp="$(mktemp)"
awk -v b="$MARK_BEGIN" -v e="$MARK_END" '
$0 == b { skip = 1; next }
skip && $0 == e { skip = 0; next }
!skip { print }
' "$HYPR_CONF" > "$tmp"
mv "$tmp" "$HYPR_CONF"
fi
# Append fresh block.
cat >> "$HYPR_CONF" <<EOF
$MARK_BEGIN
# NO-CODER — float, centered, at its design size.
windowrule = float on, match:class ^(dev\\.nocoder\\.NoCoder)$
windowrule = center on, match:class ^(dev\\.nocoder\\.NoCoder)$
windowrule = size 1280 880, match:class ^(dev\\.nocoder\\.NoCoder)$
$MARK_END
EOF
if command -v hyprctl >/dev/null 2>&1 && [[ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then
say "Reloading Hyprland"
hyprctl reload >/dev/null
else
warn "hyprctl unavailable or Hyprland not running — rules will load on next session."
fi
# Walker caches its app list — restart so new installs show up immediately.
# (Omarchy ships a helper that restarts elephant.service + walker in one go.)
if command -v omarchy-restart-walker >/dev/null 2>&1; then
say "Restarting walker so the new entry is discoverable"
omarchy-restart-walker >/dev/null 2>&1 || true
fi
cat <<EOF
${GREEN}NO-CODER installed.${RESET}
${DIM}•${RESET} App files: $INSTALL_DIR
${DIM}•${RESET} Launcher: $LAUNCHER
${DIM}•${RESET} Desktop: $DESKTOP_DIR/$APP_ID.desktop
${DIM}•${RESET} Icon: $HICOLOR_DIR/{48,64,96,128,256,512}x*/apps/$APP_ID.png
${DIM}•${RESET} Windowrules appended to $HYPR_CONF
Open the walker (Super+Space) and search for "NO-CODER".
Your git clone is no longer needed — feel free to delete it, or keep it to
'git pull && bash install.sh' for updates.
EOF