Debounce the clipboard restore so the user's clipboard is never written back while a just-pasted Vietnamese word may still be read by the target app, which caused old clipboard content to appear in the text mid-typing. Applied to both vietc-uinputd and the in-process UinputInjector. Add a GitHub Actions workflow that builds the .deb and AppImage on the runner (artifacts on push/PR, GitHub Release on v* tags), and include vietc-uinputd + vietc-xrecord in the .deb. Co-Authored-By: vndangkhoa <vonguyendangkhoa@gmail.com>
397 lines
15 KiB
Bash
397 lines
15 KiB
Bash
#!/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
|
|
|
|
# Bundle xdotool for reliable Unicode text injection
|
|
echo " Bundling xdotool..."
|
|
if command -v xdotool &>/dev/null; then
|
|
cp "$(which xdotool)" "$APPDIR/usr/bin/"
|
|
echo " xdotool bundled"
|
|
elif [ -f /tmp/xdotool-extract/usr/bin/xdotool ]; then
|
|
cp /tmp/xdotool-extract/usr/bin/xdotool "$APPDIR/usr/bin/"
|
|
echo " xdotool bundled (from extract)"
|
|
else
|
|
echo " xdotool not found, Unicode falls back to clipboard"
|
|
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'
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<component type="console-application">
|
|
<id>io.github.anomalyco.vietc</id>
|
|
<name>Viet+</name>
|
|
<summary>Vietnamese Input Method for Linux</summary>
|
|
<description>
|
|
<p>Zero-configuration Vietnamese input method engine supporting Telex and VNI input methods. Works natively on both X11 and Wayland via evdev uinput injection.</p>
|
|
</description>
|
|
<metadata_license>MIT</metadata_license>
|
|
<project_license>MIT</project_license>
|
|
<url type="homepage">https://github.com/anomalyco/vietc</url>
|
|
<provides><binary>vietc</binary></provides>
|
|
<categories><category>Utility</category></categories>
|
|
</component>
|
|
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'
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
|
|
<rect x="20" y="60" width="216" height="140" rx="16" fill="#2d2d2d" stroke="#1a1a1a" stroke-width="4"/>
|
|
<rect x="36" y="76" width="184" height="108" rx="8" fill="#3d3d3d"/>
|
|
<rect x="48" y="88" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
|
<rect x="78" y="88" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
|
<rect x="108" y="88" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
|
<rect x="138" y="88" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
|
<rect x="168" y="88" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
|
<rect x="198" y="88" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
|
<rect x="54" y="114" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
|
<rect x="84" y="114" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
|
<rect x="114" y="114" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
|
<rect x="144" y="114" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
|
<rect x="174" y="114" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
|
<rect x="60" y="140" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
|
<rect x="90" y="140" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
|
<rect x="120" y="140" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
|
<rect x="150" y="140" width="24" height="20" rx="3" fill="#f0f0f0"/>
|
|
<rect x="180" y="140" width="42" height="20" rx="3" fill="#f0f0f0"/>
|
|
<rect x="72" y="166" width="112" height="16" rx="3" fill="#f0f0f0"/>
|
|
<circle cx="216" cy="48" r="28" fill="#da251d"/>
|
|
<text x="216" y="56" text-anchor="middle" fill="white" font-size="18" font-weight="bold" font-family="sans-serif">VN</text>
|
|
</svg>
|
|
SVGEOF
|
|
fi
|
|
|
|
# Convert SVG to PNG for appimagetool (it prefers PNG for the root icon).
|
|
# This is best-effort: if no converter works, appimagetool falls back to the
|
|
# SVG, so a conversion failure must never abort the build.
|
|
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" || true
|
|
elif command -v inkscape &>/dev/null; then
|
|
inkscape -w 256 -h 256 "$APPDIR/vietc.svg" --export-filename="$APPDIR/vietc.png" 2>/dev/null || true
|
|
elif command -v convert &>/dev/null; then
|
|
convert -background none "$APPDIR/vietc.svg" -resize 256x256 "$APPDIR/vietc.png" 2>/dev/null || true
|
|
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 || true
|
|
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
|