diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7eb725f --- /dev/null +++ b/.github/workflows/release.yml @@ -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 }} diff --git a/daemon/src/main.rs b/daemon/src/main.rs index 9da825a..11c8c9b 100644 --- a/daemon/src/main.rs +++ b/daemon/src/main.rs @@ -495,6 +495,8 @@ enum OutputCommand { Backspace(usize), } +const KEY_MAX: u32 = 0x1ff; + /// Characters that flush the current word and start a new one. fn is_flush_char(ch: char) -> bool { matches!(ch, ' ' | '.' | ',' | '!' | '?' | ';' | ':' | '\t' | '\n') @@ -1280,6 +1282,17 @@ fn run_with_x11_keymap( 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); } } @@ -1443,12 +1456,40 @@ fn run_with_evdev( }; 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 = None; for event in events { - match event.kind() { - evdev::InputEventKind::Key(key) => { - let value = event.value(); - let keycode = key.0; + let ev_type = event.event_type(); + let ev_code = event.code(); + let ev_value = event.value(); + + // 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 if value == 1 { @@ -1694,19 +1735,6 @@ fn run_with_evdev( } injector.send_key_event(keycode, 0); } - } - } - _ => { - 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; - } - } } } diff --git a/install.sh b/install.sh index eb90092..22d0cdc 100755 --- a/install.sh +++ b/install.sh @@ -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}" -# 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 DISTRO="${ID:-unknown}" -echo "Detected: $DISTRO" +echo "Detected: $DISTRO ($ARCH)" -# Install dependencies -install_deps() { +install_runtime_deps() { + echo "Installing runtime dependencies..." case "$DISTRO" in ubuntu|debian|linuxmint|mint|pop|neon|zorin|elementary) export DEBIAN_FRONTEND=noninteractive 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 \ - libwayland-client0 xclip wl-clipboard + libwayland-client0 xclip wl-clipboard curl ;; fedora|rhel|centos) - dnf install -y gcc pkgconfig libX11-devel libXtst-devel dbus-devel \ - libevdev-devel libwayland-devel curl git - dnf install -y libevdev libX11 libXtst dbus-libs libwayland-client xclip wl-clipboard + dnf install -y libevdev libX11 libXtst dbus-libs libwayland-client xclip wl-clipboard curl ;; arch|manjaro) - pacman -Sy --needed --noconfirm base-devel pkgconf libx11 libxtst dbus \ - libevdev wayland curl git 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}" @@ -42,13 +44,49 @@ install_deps() { esac } -install_deps +install_runtime_deps -# Install Rust if missing -if ! command -v cargo &>/dev/null; then - echo "Installing Rust..." - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - export PATH="$HOME/.cargo/bin:$PATH" +echo "Fetching latest release..." +RELEASE_JSON=$(curl -sSfL "https://api.github.com/repos/vndangkhoa/vietc/releases/latest" 2>/dev/null || echo "") +TAG=$(echo "$RELEASE_JSON" | grep '"tag_name"' | sed 's/.*"v\(.*\)",/\1/') +if [ -z "$TAG" ]; then + 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 # 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 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 echo "Installing to /usr/bin/..." -cp target/release/vietc /usr/bin/vietc-daemon -cp target/release/vietc-cli /usr/bin/ -cp target/release/vietc-uinputd /usr/bin/ -cp ui/target/release/vietc-tray /usr/bin/ -[ -f target/release/vietc-xrecord ] && cp target/release/vietc-xrecord /usr/bin/ +cp "$BIN_DIR/vietc-daemon" /usr/bin/vietc-daemon +cp "$BIN_DIR/vietc-cli" /usr/bin/vietc-cli +cp "$BIN_DIR/vietc-uinputd" /usr/bin/vietc-uinputd +cp "$BIN_DIR/vietc-tray" /usr/bin/vietc-tray +[ -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 # Clean old /usr/local/bin/ binaries 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 -# Udev rules for uinput +# Udev rules echo 'KERNEL=="uinput", GROUP="input", MODE="0660"' > /etc/udev/rules.d/99-vietc.rules udevadm control --reload-rules 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 INSTALLING_USER="${SUDO_USER:-$USER}" 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 fi -# Create default config +# Config 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" toggle_key = "space" -toggle_method_key = "shift" start_enabled = 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] enabled = true english_apps = ["code", "vim"] 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 +fi echo -e "${GREEN}=== Done! ===${NC}" echo -e "${YELLOW}Log out and log back in, then run: vietc-tray${NC}" diff --git a/uninstall.sh b/uninstall.sh index 46763bd..b8afd82 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -38,7 +38,12 @@ rm -f /usr/share/icons/hicolor/256x256/apps/vietc*.svg rm -f /usr/share/applications/vietc.desktop rm -f /etc/xdg/autostart/vietc-tray.desktop -# Reload +# Reload udev 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}"