#!/usr/bin/env bash set -euo pipefail # Ensure cargo is in PATH if ! command -v cargo &>/dev/null; then if [ -f "$HOME/.cargo/bin/cargo" ]; then export PATH="$HOME/.cargo/bin:$PATH" fi fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" APPDIR="$SCRIPT_DIR/AppDir" VERSION="${1:-0.1.1}" echo "=== Building Viet+ AppImage v${VERSION} ===" # Clean rm -rf "$APPDIR" mkdir -p "$APPDIR/usr/bin" mkdir -p "$APPDIR/usr/share/applications" mkdir -p "$APPDIR/usr/share/icons/hicolor/256x256/apps" mkdir -p "$APPDIR/usr/share/doc/vietc" mkdir -p "$APPDIR/etc/vietc" mkdir -p "$APPDIR/usr/lib/systemd/user" mkdir -p "$APPDIR/usr/share/metainfo" # Build binaries echo "[1/5] Building binaries..." if [ ! -f "target/release/vietc" ]; then cargo build --release cd "$PROJECT_ROOT/ui" && cargo build --release && cd "$PROJECT_ROOT" fi echo " Built with x11 + wayland" # Copy binaries from deb-build if they exist, otherwise from target/release echo "[2/5] Installing binaries..." if [ -d "deb-build/usr/bin" ]; then cp -r deb-build/usr/bin/* "$APPDIR/usr/bin/" else cp target/release/vietc "$APPDIR/usr/bin/" cp target/release/vietc-cli "$APPDIR/usr/bin/" cp target/release/vietc-uinputd "$APPDIR/usr/bin/" [ -f ui/target/release/vietc-tray ] && cp ui/target/release/vietc-tray "$APPDIR/usr/bin/" fi # Bundle xclip as fallback for clipboard operations echo " Bundling xclip..." if command -v xclip &>/dev/null; then cp "$(which xclip)" "$APPDIR/usr/bin/" echo " xclip bundled" else echo " xclip not found on system, skipping" fi # Compile and bundle vietc-xrecord (C helper for XRecord keyboard capture) echo " Compiling vietc-xrecord..." if command -v gcc &>/dev/null; then gcc -O2 -o "$APPDIR/usr/bin/vietc-xrecord" "$SCRIPT_DIR/vietc-xrecord.c" -lX11 -lXtst echo " vietc-xrecord bundled" elif command -v cc &>/dev/null; then cc -O2 -o "$APPDIR/usr/bin/vietc-xrecord" "$SCRIPT_DIR/vietc-xrecord.c" -lX11 -lXtst echo " vietc-xrecord bundled" else echo " WARNING: No C compiler found, vietc-xrecord not bundled — X11 capture will fail" fi # Desktop integration echo "[3/5] Installing desktop integration..." if [ -f "deb-build/vietc.desktop" ]; then cp deb-build/vietc.desktop "$APPDIR/usr/share/applications/" else cp "$SCRIPT_DIR/vietc.desktop" "$APPDIR/usr/share/applications/" fi # Icons if [ -f "deb-build/vietc.svg" ]; then cp deb-build/vietc.svg "$APPDIR/usr/share/icons/hicolor/256x256/apps/" cp deb-build/vietc.png "$APPDIR/usr/share/icons/hicolor/256x256/apps/" cp deb-build/vietc.png "$APPDIR/" fi # AppStream metadata if [ -f "deb-build/usr/share/metainfo/io.github.anomalyco.vietc.appdata.xml" ]; then cp deb-build/usr/share/metainfo/io.github.anomalyco.vietc.appdata.xml "$APPDIR/usr/share/metainfo/" else cat > "$APPDIR/usr/share/metainfo/io.github.anomalyco.vietc.appdata.xml" << 'XML' io.github.anomalyco.vietc Viet+ Vietnamese Input Method for Linux

Zero-configuration Vietnamese input method engine supporting Telex and VNI input methods. Works natively on both X11 and Wayland via evdev uinput injection.

MIT MIT https://github.com/anomalyco/vietc vietc Utility
XML fi # Config echo "[4/5] Installing config..." if [ -f "deb-build/etc/vietc/config.toml" ]; then cp deb-build/etc/vietc/config.toml "$APPDIR/etc/vietc/" else sed 's/^grab = false/grab = true/' "$PROJECT_ROOT/vietc.toml" > "$APPDIR/etc/vietc/config.toml" fi # Docs if [ -f "deb-build/usr/share/doc/vietc/README.md" ]; then cp deb-build/usr/share/doc/vietc/README.md "$APPDIR/usr/share/doc/vietc/" else cp "$PROJECT_ROOT/README.md" "$APPDIR/usr/share/doc/vietc/" fi # Systemd service if [ -f "deb-build/usr/lib/systemd/user/vietc.service" ]; then cp deb-build/usr/lib/systemd/user/vietc.service "$APPDIR/usr/lib/systemd/user/" else cp "$PROJECT_ROOT/vietc.service" "$APPDIR/usr/lib/systemd/user/" fi # Desktop file in AppDir root if [ -f "deb-build/vietc.desktop" ]; then cp deb-build/vietc.desktop "$APPDIR/" else cp "$APPDIR/usr/share/applications/vietc.desktop" "$APPDIR/" fi # Icon — required by appimagetool (desktop file has Icon=vietc) # Use SVG from deb build if available, otherwise generate a keyboard icon if [ -f "deb-build/usr/share/icons/hicolor/256x256/apps/vietc.svg" ]; then cp "deb-build/usr/share/icons/hicolor/256x256/apps/vietc.svg" "$APPDIR/vietc.svg" elif [ -f "deb-build/usr/share/icons/hicolor/256x256/apps/vietc.png" ]; then cp "deb-build/usr/share/icons/hicolor/256x256/apps/vietc.png" "$APPDIR/vietc.png" else # Generate a proper keyboard+VN icon as SVG cat > "$APPDIR/vietc.svg" << 'SVGEOF' VN SVGEOF fi # Convert SVG to PNG for appimagetool (it prefers PNG for the root icon) if [ -f "$APPDIR/vietc.svg" ] && ! [ -f "$APPDIR/vietc.png" ]; then if command -v rsvg-convert &>/dev/null; then rsvg-convert -w 256 -h 256 "$APPDIR/vietc.svg" -o "$APPDIR/vietc.png" elif command -v inkscape &>/dev/null; then inkscape -w 256 -h 256 "$APPDIR/vietc.svg" --export-filename="$APPDIR/vietc.png" 2>/dev/null elif command -v convert &>/dev/null; then convert -background none "$APPDIR/vietc.svg" -resize 256x256 "$APPDIR/vietc.png" 2>/dev/null elif command -v python3 &>/dev/null; then python3 -c " import subprocess, sys try: subprocess.check_call(['rsvg-convert', '-w', '256', '-h', '256', '$APPDIR/vietc.svg', '-o', '$APPDIR/vietc.png']) except Exception: pass " 2>/dev/null fi # If no converter, appimagetool can use SVG directly fi # Also put icon in hicolor for system installs via AppImage mkdir -p "$APPDIR/usr/share/icons/hicolor/256x256/apps" [ -f "$APPDIR/vietc.svg" ] && cp "$APPDIR/vietc.svg" "$APPDIR/usr/share/icons/hicolor/256x256/apps/" [ -f "$APPDIR/vietc.png" ] && cp "$APPDIR/vietc.png" "$APPDIR/usr/share/icons/hicolor/256x256/apps/" # Create custom AppRun script cat > "$APPDIR/AppRun" << 'EOF' #!/bin/sh HERE="$(dirname "$(readlink -f "${0}")")" # Handle --update flag: download latest AppImage from GitHub if [ "$1" = "--update" ]; then echo "Viet+ Self-Updater" RELEASE_URL="https://github.com/vndangkhoa/vietc/releases/latest/download/Viet+-x86_64.AppImage" TEMP="/tmp/Viet+-update.AppImage" echo "Downloading latest release..." if command -v curl >/dev/null 2>&1; then curl -L -o "$TEMP" "$RELEASE_URL" 2>/dev/null elif command -v wget >/dev/null 2>&1; then wget -q -O "$TEMP" "$RELEASE_URL" 2>/dev/null else echo "ERROR: curl or wget required for updates" >&2 exit 1 fi if [ -s "$TEMP" ]; then chmod +x "$TEMP" mv "$TEMP" "$(readlink -f "${0}")" echo "Updated! Restart the AppImage." else echo "ERROR: Download failed" >&2 fi exit 0 fi # Handle --quit flag: stop daemon and uinputd if [ "$1" = "--quit" ]; then echo "Stopping Viet+..." pkill -x vietc-tray 2>/dev/null pkill -x vietc 2>/dev/null pkill -x vietc-uinputd 2>/dev/null pkill -x vietc-xrecord 2>/dev/null sleep 0.5 echo "Viet+ stopped." exit 0 fi # Handle --restart flag if [ "$1" = "--restart" ]; then pkill -x vietc-tray 2>/dev/null pkill -x vietc 2>/dev/null pkill -x vietc-uinputd 2>/dev/null pkill -x vietc-xrecord 2>/dev/null sleep 0.5 # Fall through to normal start below fi # Export our bin dir on PATH so child processes can find sibling binaries export PATH="$HERE/usr/bin:$PATH" # Build display env prefix for elevation commands. # Capture from current user env (DISPLAY, XAUTHORITY, WAYLAND_DISPLAY, XDG_RUNTIME_DIR) # so they are available inside the root daemon. Without this, xdotool/xclip/wtype # fail silently because sudo/pkexec strip display env vars. ENV_PREFIX="env" [ -n "$DISPLAY" ] && ENV_PREFIX="$ENV_PREFIX DISPLAY=$DISPLAY" [ -n "$XAUTHORITY" ] && ENV_PREFIX="$ENV_PREFIX XAUTHORITY=$XAUTHORITY" [ -n "$WAYLAND_DISPLAY" ] && ENV_PREFIX="$ENV_PREFIX WAYLAND_DISPLAY=$WAYLAND_DISPLAY" [ -n "$XDG_RUNTIME_DIR" ] && ENV_PREFIX="$ENV_PREFIX XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR" # Ensure system library paths are available for dlopen (libX11, libXtst, etc.) # AppImage runtime may override LD_LIBRARY_PATH; append system paths as fallback SYSLIB_PATHS="/usr/lib/x86_64-linux-gnu:/usr/lib64:/usr/lib:/lib/x86_64-linux-gnu:/lib64:/lib" if [ -n "$LD_LIBRARY_PATH" ]; then export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$SYSLIB_PATHS" else export LD_LIBRARY_PATH="$SYSLIB_PATHS" fi # Start daemon (kill old non-root one first if we have root) # On X11 we can run without root (XGrabKeyboard + XTest injection needs no special permissions). # On Wayland, evdev requires root (input group) or uinput. NEED_ROOT="" if [ -n "$WAYLAND_DISPLAY" ]; then NEED_ROOT="yes" fi if [ -z "$NEED_ROOT" ]; then # X11: no root needed for capture, but uinputd needs root for injection pkill -x vietc-uinputd 2>/dev/null pkill -x vietc 2>/dev/null; sleep 0.3 mkdir -p "$HOME/.config/vietc" "$HOME/.vietc" # Try to start the uinputd daemon (preferred injection path) if command -v pkexec >/dev/null 2>&1; then pkexec "$HERE/usr/bin/vietc-uinputd" >/dev/null 2>&1 & UINPUTD_PID=$! sleep 0.3 elif command -v sudo >/dev/null 2>&1; then if sudo -n true 2>/dev/null; then sudo "$HERE/usr/bin/vietc-uinputd" >/dev/null 2>&1 & UINPUTD_PID=$! sleep 0.3 fi fi "$HERE/usr/bin/vietc" >"$HOME/.config/vietc/vietc-daemon.log" 2>&1 & DAEMON_PID=$! echo "[vietc] Daemon started (PID=$DAEMON_PID), log: $HOME/.config/vietc/vietc-daemon.log" else # Fix Wayland env for root: sudo resets XDG_RUNTIME_DIR, breaking wtype/wl-copy. if [ "$(id -u)" = "0" ] && [ -z "$XDG_RUNTIME_DIR" ] && [ -n "$SUDO_USER" ]; then USER_UID=$(id -u "$SUDO_USER" 2>/dev/null || echo 1000) export XDG_RUNTIME_DIR="/run/user/$USER_UID" if [ -d "/run/user/$USER_UID" ] && ls "/run/user/$USER_UID/wayland-*" >/dev/null 2>&1; then export WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-0}" fi fi if command -v pkexec >/dev/null; then pkill -x vietc 2>/dev/null; sleep 0.5 pkexec $ENV_PREFIX "$HERE/usr/bin/vietc" >/dev/null & DAEMON_PID=$! elif [ -n "$WAYLAND_DISPLAY" ]; then password="" if command -v kdialog >/dev/null; then password=$(kdialog --password "Viet+ needs root privileges to grab the keyboard.") || password="" elif command -v zenity >/dev/null; then password=$(zenity --password --title="Viet+ needs root") || password="" elif command -v ssh-askpass >/dev/null; then password=$(ssh-askpass "Viet+ needs root privileges") || password="" fi if [ -n "$password" ]; then pkill -x vietc 2>/dev/null; sleep 0.5 echo "$password" | sudo -S $ENV_PREFIX "$HERE/usr/bin/vietc" >/dev/null & DAEMON_PID=$! fi elif command -v sudo >/dev/null; then pkill -x vietc 2>/dev/null; sleep 0.5 sudo $ENV_PREFIX "$HERE/usr/bin/vietc" >/dev/null & DAEMON_PID=$! fi fi if [ -z "$DAEMON_PID" ] && ! pgrep -x vietc >/dev/null; then mkdir -p "$HOME/.config/vietc" "$HERE/usr/bin/vietc" >"$HOME/.config/vietc/vietc-daemon.log" 2>&1 & DAEMON_PID=$! echo "[vietc] Daemon fallback started (PID=$DAEMON_PID), log: $HOME/.config/vietc/vietc-daemon.log" fi # Keep the AppImage alive with a tray or settings UI. # Run as a child (not exec) so daemon cleanup works on exit. cleanup_daemon() { if [ -n "$DAEMON_PID" ]; then kill "$DAEMON_PID" 2>/dev/null wait "$DAEMON_PID" 2>/dev/null fi if [ -n "$UINPUTD_PID" ]; then kill "$UINPUTD_PID" 2>/dev/null wait "$UINPUTD_PID" 2>/dev/null fi } trap cleanup_daemon EXIT INT TERM if [ -f "$HERE/usr/bin/vietc-tray" ]; then "$HERE/usr/bin/vietc-tray" "$@" else echo "[vietc] Daemon running (PID=$DAEMON_PID)." echo "[vietc] To stop: Vietnam+-*.AppImage --quit" # Show a quit dialog if DE is available if command -v zenity >/dev/null 2>&1; then zenity --info --text="Viet+ is running in the background.\n\nTo stop: run this AppImage with --quit" \ --title="Viet+ Input Method" --width=350 2>/dev/null & fi # Keep alive until daemon dies or user quits via --quit wait $DAEMON_PID 2>/dev/null fi EOF chmod +x "$APPDIR/AppRun" echo "[5/5] AppDir ready at: $APPDIR" echo "" # Auto build if appimagetool exists if [ -f "$SCRIPT_DIR/appimagetool" ]; then echo "=== Running appimagetool FUSE build ===" ARCH=x86_64 "$SCRIPT_DIR/appimagetool" --appimage-extract-and-run "$APPDIR" "$SCRIPT_DIR/Viet+-${VERSION}-x86_64.AppImage" elif command -v appimagetool &>/dev/null; then echo "=== Running system appimagetool ===" ARCH=x86_64 appimagetool "$APPDIR" "$SCRIPT_DIR/Viet+-${VERSION}-x86_64.AppImage" else echo "To build AppImage:" echo " appimagetool $APPDIR Viet+-${VERSION}-x86_64.AppImage" fi