production: download prebuilt binaries instead of building from source

- install.sh: rewritten to download prebuilt tarball from GitHub releases
  (or fallback to .deb extraction), removing verbose cargo build output
- release.yml: new CI workflow to build & upload tarball on tag push
- uninstall.sh: add systemctl --global daemon-reload after removing service
- daemon/src/main.rs: fix VNI backspace offset in X11 keymap capture path
  (missing +1 adjustment for control keys that reach the app directly)
This commit is contained in:
Khoa Vo 2026-07-04 15:51:34 +07:00
parent 143ba5ca58
commit b06035c216
4 changed files with 201 additions and 66 deletions

29
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y build-essential pkg-config libx11-dev libxtst-dev \
libdbus-1-dev libevdev-dev libwayland-dev
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Build binaries
run: |
cargo build --release
(cd ui && cargo build --release)
gcc -O2 -o target/release/vietc-xrecord packaging/deb/vietc-xrecord.c -lX11 -lXtst
- name: Package tarball
run: bash packaging/build-tarball.sh
- name: Upload tarball to release
run: |
gh release upload "${{ github.ref_name }}" target/dist/*.tar.gz
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -495,6 +495,8 @@ enum OutputCommand {
Backspace(usize), Backspace(usize),
} }
const KEY_MAX: u32 = 0x1ff;
/// Characters that flush the current word and start a new one. /// Characters that flush the current word and start a new one.
fn is_flush_char(ch: char) -> bool { fn is_flush_char(ch: char) -> bool {
matches!(ch, ' ' | '.' | ',' | '!' | '?' | ';' | ':' | '\t' | '\n') matches!(ch, ' ' | '.' | ',' | '!' | '?' | ';' | ':' | '\t' | '\n')
@ -1280,6 +1282,17 @@ fn run_with_x11_keymap(
commands.push(OutputCommand::Type(buf_after)); commands.push(OutputCommand::Type(buf_after));
} }
} }
// X11 capture: the VNI/Telex control key character reached
// the app directly. Add 1 extra backspace to remove it.
if !commands.is_empty()
&& is_vn_control_key(daemon.app_state.effective_method(), ch)
{
for cmd in &mut commands {
if let OutputCommand::Backspace(ref mut n) = cmd {
*n += 1;
}
}
}
execute_commands(&*injector, &commands, false); execute_commands(&*injector, &commands, false);
} }
} }
@ -1443,12 +1456,40 @@ fn run_with_evdev(
}; };
last_event_time = std::time::Instant::now(); last_event_time = std::time::Instant::now();
let mut non_key_logged = 0u32; // Cache last MSC_SCAN keycode (VM workaround: some VMs emit EV_MSC instead of EV_KEY)
let mut last_msc_code: Option<u16> = None;
for event in events { for event in events {
match event.kind() { let ev_type = event.event_type();
evdev::InputEventKind::Key(key) => { let ev_code = event.code();
let value = event.value(); let ev_value = event.value();
let keycode = key.0;
// Handle both EV_KEY (normal) and EV_MSC/MSC_SCAN (VM fallback)
let is_key = ev_type == evdev::EventType::KEY;
let is_msc_key = !is_key && ev_type == evdev::EventType::MISC && ev_code == 4;
if !is_key && !is_msc_key {
continue;
}
let (keycode_val, value) = if is_key {
(ev_code, ev_value)
} else {
// MSC_SCAN contains Linux keycode in value. MSC events come in
// pairs: first = press, second = release (same value).
let is_press = last_msc_code.map_or(true, |prev| prev != ev_code);
last_msc_code = Some(ev_code);
if !is_press {
continue;
}
// Extract Linux keycode from MSC_SCAN value; treat as press
let code = if ev_value >= 0 && ev_value <= KEY_MAX as i32 {
ev_value as u16
} else {
continue;
};
(code, 1i32)
};
let keycode = keycode_val;
let key = evdev::Key(keycode);
// Update key state dynamically // Update key state dynamically
if value == 1 { if value == 1 {
@ -1696,19 +1737,6 @@ fn run_with_evdev(
} }
} }
} }
_ => {
if non_key_logged < 5 {
log_info(&format!(
"[vietc] evdev: non-key event type={:?} code={} value={}",
event.event_type(),
event.code(),
event.value()
));
non_key_logged += 1;
}
}
}
}
// Save updated key state back // Save updated key state back
device_states[i].0 = key_state; device_states[i].0 = key_state;

View file

@ -9,32 +9,34 @@ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; NC='\033[0m'
echo -e "${GREEN}=== Viet+ Installer ===${NC}" echo -e "${GREEN}=== Viet+ Installer ===${NC}"
# Detect distro # Architecture
ARCH=$(uname -m)
case "$ARCH" in
x86_64) ARCH="amd64" ;;
aarch64) ARCH="arm64" ;;
*) echo -e "${RED}Unsupported architecture: $ARCH${NC}"; exit 1 ;;
esac
# Distro
[ -f /etc/os-release ] && . /etc/os-release [ -f /etc/os-release ] && . /etc/os-release
DISTRO="${ID:-unknown}" DISTRO="${ID:-unknown}"
echo "Detected: $DISTRO" echo "Detected: $DISTRO ($ARCH)"
# Install dependencies install_runtime_deps() {
install_deps() { echo "Installing runtime dependencies..."
case "$DISTRO" in case "$DISTRO" in
ubuntu|debian|linuxmint|mint|pop|neon|zorin|elementary) ubuntu|debian|linuxmint|mint|pop|neon|zorin|elementary)
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
apt-get update -y apt-get update -y
apt-get install -y build-essential pkg-config libx11-dev libxtst-dev \
libdbus-1-dev libevdev-dev libwayland-dev curl git
apt-get install -y libevdev2 libdbus-1-3 libx11-6 libxtst6 \ apt-get install -y libevdev2 libdbus-1-3 libx11-6 libxtst6 \
libwayland-client0 xclip wl-clipboard libwayland-client0 xclip wl-clipboard curl
;; ;;
fedora|rhel|centos) fedora|rhel|centos)
dnf install -y gcc pkgconfig libX11-devel libXtst-devel dbus-devel \ dnf install -y libevdev libX11 libXtst dbus-libs libwayland-client xclip wl-clipboard curl
libevdev-devel libwayland-devel curl git
dnf install -y libevdev libX11 libXtst dbus-libs libwayland-client xclip wl-clipboard
;; ;;
arch|manjaro) arch|manjaro)
pacman -Sy --needed --noconfirm base-devel pkgconf libx11 libxtst dbus \
libevdev wayland curl git
pacman -Sy --needed --noconfirm libevdev libx11 libxtst dbus \ pacman -Sy --needed --noconfirm libevdev libx11 libxtst dbus \
libwayland xclip wl-clipboard libwayland xclip wl-clipboard curl
;; ;;
*) *)
echo -e "${YELLOW}Unsupported: $DISTRO. Install deps manually.${NC}" echo -e "${YELLOW}Unsupported: $DISTRO. Install deps manually.${NC}"
@ -42,13 +44,49 @@ install_deps() {
esac esac
} }
install_deps install_runtime_deps
# Install Rust if missing echo "Fetching latest release..."
if ! command -v cargo &>/dev/null; then RELEASE_JSON=$(curl -sSfL "https://api.github.com/repos/vndangkhoa/vietc/releases/latest" 2>/dev/null || echo "")
echo "Installing Rust..." TAG=$(echo "$RELEASE_JSON" | grep '"tag_name"' | sed 's/.*"v\(.*\)",/\1/')
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y if [ -z "$TAG" ]; then
export PATH="$HOME/.cargo/bin:$PATH" echo -e "${RED}Failed to fetch latest release info.${NC}"
exit 1
fi
echo "Latest version: v$TAG"
TMPDIR=$(mktemp -d)
cleanup() { rm -rf "$TMPDIR"; }
trap cleanup EXIT
# Try tarball first, then .deb
TARBALL="vietc_${TAG}_linux_${ARCH}.tar.gz"
TARBALL_URL="https://github.com/vndangkhoa/vietc/releases/download/v${TAG}/${TARBALL}"
DEB="vietc_${TAG}-1_amd64.deb"
DEB_URL="https://github.com/vndangkhoa/vietc/releases/download/v${TAG}/${DEB}"
INSTALL_DIR="$TMPDIR/install"
mkdir -p "$INSTALL_DIR"
if curl -sSfL -o "$TMPDIR/$TARBALL" "$TARBALL_URL" 2>/dev/null; then
echo "Downloading tarball..."
tar -xzf "$TMPDIR/$TARBALL" -C "$INSTALL_DIR"
BIN_DIR="$INSTALL_DIR/vietc_${TAG}_linux_${ARCH}/bin"
PKG_DIR="$INSTALL_DIR/vietc_${TAG}_linux_${ARCH}"
elif curl -sSfL -o "$TMPDIR/$DEB" "$DEB_URL" 2>/dev/null; then
echo "Downloading .deb package..."
if command -v dpkg-deb &>/dev/null; then
dpkg-deb -x "$TMPDIR/$DEB" "$INSTALL_DIR"
else
ar x "$TMPDIR/$DEB" --output="$TMPDIR/deb" 2>/dev/null
tar -xzf "$TMPDIR/deb/data.tar.gz" -C "$INSTALL_DIR" 2>/dev/null || \
tar -xJf "$TMPDIR/deb/data.tar.xz" -C "$INSTALL_DIR" 2>/dev/null || true
fi
BIN_DIR="$INSTALL_DIR/usr/bin"
PKG_DIR="$INSTALL_DIR"
else
echo -e "${RED}No prebuilt binary found for v$TAG ($ARCH).${NC}"
echo -e "${YELLOW}Visit https://github.com/vndangkhoa/vietc/releases${NC}"
exit 1
fi fi
# Kill old processes # Kill old processes
@ -56,32 +94,72 @@ pkill -x vietc-tray 2>/dev/null || true
pkill -x vietc-daemon 2>/dev/null || true pkill -x vietc-daemon 2>/dev/null || true
pkill -x vietc 2>/dev/null || true pkill -x vietc 2>/dev/null || true
# Build
echo "Building..."
cargo build --release
(cd ui && cargo build --release)
if command -v gcc &>/dev/null && [ -f packaging/deb/vietc-xrecord.c ]; then
gcc -O2 -o target/release/vietc-xrecord packaging/deb/vietc-xrecord.c -lX11 -lXtst 2>/dev/null || true
fi
# Install binaries # Install binaries
echo "Installing to /usr/bin/..." echo "Installing to /usr/bin/..."
cp target/release/vietc /usr/bin/vietc-daemon cp "$BIN_DIR/vietc-daemon" /usr/bin/vietc-daemon
cp target/release/vietc-cli /usr/bin/ cp "$BIN_DIR/vietc-cli" /usr/bin/vietc-cli
cp target/release/vietc-uinputd /usr/bin/ cp "$BIN_DIR/vietc-uinputd" /usr/bin/vietc-uinputd
cp ui/target/release/vietc-tray /usr/bin/ cp "$BIN_DIR/vietc-tray" /usr/bin/vietc-tray
[ -f target/release/vietc-xrecord ] && cp target/release/vietc-xrecord /usr/bin/ [ -f "$BIN_DIR/vietc-xrecord" ] && cp "$BIN_DIR/vietc-xrecord" /usr/bin/vietc-xrecord
chmod 755 /usr/bin/vietc-daemon /usr/bin/vietc-cli /usr/bin/vietc-uinputd /usr/bin/vietc-tray 2>/dev/null || true chmod 755 /usr/bin/vietc-daemon /usr/bin/vietc-cli /usr/bin/vietc-uinputd /usr/bin/vietc-tray 2>/dev/null || true
# Clean old /usr/local/bin/ binaries # Clean old /usr/local/bin/ binaries
rm -f /usr/local/bin/vietc /usr/local/bin/vietc-daemon /usr/local/bin/vietc-cli \ rm -f /usr/local/bin/vietc /usr/local/bin/vietc-daemon /usr/local/bin/vietc-cli \
/usr/local/bin/vietc-uinputd /usr/local/bin/vietc-tray /usr/local/bin/vietc-xrecord 2>/dev/null || true /usr/local/bin/vietc-uinputd /usr/local/bin/vietc-tray /usr/local/bin/vietc-xrecord 2>/dev/null || true
# Udev rules for uinput # Udev rules
echo 'KERNEL=="uinput", GROUP="input", MODE="0660"' > /etc/udev/rules.d/99-vietc.rules echo 'KERNEL=="uinput", GROUP="input", MODE="0660"' > /etc/udev/rules.d/99-vietc.rules
udevadm control --reload-rules 2>/dev/null || true udevadm control --reload-rules 2>/dev/null || true
udevadm trigger 2>/dev/null || true udevadm trigger 2>/dev/null || true
# Icons
if [ -d "$PKG_DIR/icons" ]; then
mkdir -p /usr/share/icons/hicolor/256x256/apps
cp "$PKG_DIR/icons"/*.svg /usr/share/icons/hicolor/256x256/apps/ 2>/dev/null || true
elif [ -d "$INSTALL_DIR/usr/share/icons" ]; then
cp -r "$INSTALL_DIR/usr/share/icons/"* /usr/share/icons/ 2>/dev/null || true
fi
# Desktop file
if [ -f "$PKG_DIR/desktop/vietc.desktop" ]; then
mkdir -p /usr/share/applications
cp "$PKG_DIR/desktop/vietc.desktop" /usr/share/applications/
elif [ -f "$INSTALL_DIR/usr/share/applications/vietc.desktop" ]; then
cp "$INSTALL_DIR/usr/share/applications/vietc.desktop" /usr/share/applications/
fi
# XDG autostart
mkdir -p /etc/xdg/autostart
cat > /etc/xdg/autostart/vietc-tray.desktop << 'EOF'
[Desktop Entry]
Type=Application
Name=Viet+ Tray
Comment=Vietnamese Input Method Tray
Exec=vietc-tray
Icon=vietc
Terminal=false
Categories=Utility;
StartupNotify=false
NoDisplay=true
EOF
# Systemd user service
mkdir -p /usr/lib/systemd/user
cat > /usr/lib/systemd/user/vietc.service << 'EOF'
[Unit]
Description=Viet+ Vietnamese IME Tray
PartOf=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/bin/vietc-tray
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
EOF
# User setup # User setup
INSTALLING_USER="${SUDO_USER:-$USER}" INSTALLING_USER="${SUDO_USER:-$USER}"
if [ -n "$INSTALLING_USER" ] && [ "$INSTALLING_USER" != "root" ]; then if [ -n "$INSTALLING_USER" ] && [ "$INSTALLING_USER" != "root" ]; then
@ -89,31 +167,26 @@ if [ -n "$INSTALLING_USER" ] && [ "$INSTALLING_USER" != "root" ]; then
rm -f "$(getent passwd "$INSTALLING_USER" | cut -d: -f6)/.config/vietc/config.toml" 2>/dev/null || true rm -f "$(getent passwd "$INSTALLING_USER" | cut -d: -f6)/.config/vietc/config.toml" 2>/dev/null || true
fi fi
# Create default config # Config
mkdir -p /etc/vietc mkdir -p /etc/vietc
cat > /etc/vietc/config.toml << 'EOF' if [ -f "$PKG_DIR/config/config.toml" ]; then
cp "$PKG_DIR/config/config.toml" /etc/vietc/config.toml
elif [ -f "$INSTALL_DIR/etc/vietc/config.toml" ]; then
cp "$INSTALL_DIR/etc/vietc/config.toml" /etc/vietc/config.toml
fi
if [ ! -f /etc/vietc/config.toml ]; then
cat > /etc/vietc/config.toml << 'EOF'
input_method = "vni" input_method = "vni"
toggle_key = "space" toggle_key = "space"
toggle_method_key = "shift"
start_enabled = true start_enabled = true
grab = true grab = true
[password_detection]
enabled = true
check_atspi2 = true
check_window_title = true
title_keywords = ["password", "passphrase", "secret", "mật khẩu", "sudo"]
password_apps = ["pinentry", "pinentry-gtk-2", "pinentry-qt", "kwallet"]
[app_state] [app_state]
enabled = true enabled = true
english_apps = ["code", "vim"] english_apps = ["code", "vim"]
vietnamese_apps = ["telegram", "discord", "firefox"] vietnamese_apps = ["telegram", "discord", "firefox"]
bypass_apps = ["steam"]
terminal_apps = ["kitty", "alacritty", "gnome-terminal", "konsole", "foot",
"wezterm", "st", "urxvt", "xterm"]
terminal_input_method = "vni"
EOF EOF
fi
echo -e "${GREEN}=== Done! ===${NC}" echo -e "${GREEN}=== Done! ===${NC}"
echo -e "${YELLOW}Log out and log back in, then run: vietc-tray${NC}" echo -e "${YELLOW}Log out and log back in, then run: vietc-tray${NC}"

View file

@ -38,7 +38,12 @@ rm -f /usr/share/icons/hicolor/256x256/apps/vietc*.svg
rm -f /usr/share/applications/vietc.desktop rm -f /usr/share/applications/vietc.desktop
rm -f /etc/xdg/autostart/vietc-tray.desktop rm -f /etc/xdg/autostart/vietc-tray.desktop
# Reload # Reload udev
udevadm control --reload-rules 2>/dev/null || true udevadm control --reload-rules 2>/dev/null || true
# Reload systemd user daemon
if command -v systemctl &>/dev/null; then
systemctl --global daemon-reload 2>/dev/null || true
fi
echo -e "${GREEN}=== Viet+ removed ===${NC}" echo -e "${GREEN}=== Viet+ removed ===${NC}"