From 98ce9def7919da07916637fa67928830609038ce Mon Sep 17 00:00:00 2001 From: Khoa Vo Date: Mon, 29 Jun 2026 14:32:30 +0700 Subject: [PATCH] feat: Flatpak tray, X11 dlopen window query, desktop menu entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add system tray (vietc-tray) to Flatpak build; command changed to vietc-tray which spawns the daemon - Desktop menu entry: Viet+ appears in app launcher for search/install/uninstall - Tray fixes: find_sibling_binary tries {name}-daemon fallback for Flatpak; is_daemon_running checks both vietc and vietc-daemon process names - Native X11 _NET_ACTIVE_WINDOW via dlopen(libX11.so.6) — third fallback in get_active_window_id() that works inside Flatpak sandbox (no xdotool/xprop) - Update README with install/uninstall commands - Update CHANGELOG --- CHANGELOG.md | 11 ++- README.md | 11 ++- daemon/src/app_state.rs | 113 +++++++++++++++++++++++++++++ packaging/flatpak/build-flatpak.sh | 12 +-- ui/src/main.rs | 25 +++++-- 5 files changed, 156 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d2858f..31466d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,9 +14,16 @@ - **Engine dead code removed** — unused methods `is_empty`, `is_tone_or_mark_key`, `process_string`, `last_base_char`, `apply_cluster_mark`, `apply_mark` in `BambooEngine`; `RuleEffect` enum and `special_rules` field in `InputMethodRules`. - **Production logging** — per-key `eprintln!` removed from evdev loop and uinput paste path. Only startup/error/window-change messages remain (`log_info` to both stderr and file). -### Flatpak Build +### Flatpak Build & System Tray +- **System tray** (`vietc-tray` using ksni/DBus StatusNotifier) is now built and included in the Flatpak bundle. The tray launches the daemon and shows Vietnamese/English mode. +- **Desktop menu entry** — the app now appears when searching **"Viet+"** in the application menu. Search, launch, or uninstall from there. +- **Flatpak command** changed from `vietc-daemon` to `vietc-tray` (the tray spawns the daemon). +- **Tray fixes for Flatpak** — `find_sibling_binary()` now tries `{name}-daemon` fallback; `is_daemon_running()` checks both `vietc` and `vietc-daemon` process names. - **Fixed `mkdir -p`** — `build-flatpak.sh` now creates `/app/share/applications` before installing the desktop file. -- **Bundle**: `VietPlus-0.1.5.flatpak` (47 MB, runtime `org.gnome.Platform//50`). Warning-free build with default Rust profile (no `#![allow()]` needed). + +### Active Window Detection (Flatpak fix) +- **Native X11 `_NET_ACTIVE_WINDOW` query** via `dlopen("libX11.so.6")` — added as third fallback in `get_active_window_id()`. Works inside the Flatpak sandbox where `xdotool`/`xprop` are unavailable. No subprocess, no external dependencies. +- **Bundle**: `VietPlus-0.1.5.flatpak` (66 MB with tray, runtime `org.gnome.Platform//50`). Warning-free build. --- diff --git a/README.md b/README.md index 95edfbb..7b5cc11 100644 --- a/README.md +++ b/README.md @@ -270,10 +270,17 @@ Flexible typing: type the full syllable, then add marks/tone keys at the end. Ex ### Flatpak (recommended) +System tray icon + daemon. Find **"Viet+"** in your app menu to launch, or run from terminal. + ```bash -# Download from the release page, then: +# Install flatpak install --user --bundle VietPlus-x86_64.flatpak + +# Launch via app menu, or: flatpak run io.github.vietc.VietPlus + +# Uninstall +flatpak uninstall --user io.github.vietc.VietPlus ``` Includes daemon + CLI + system tray + uinput daemon. Sandboxed — no system libraries are touched. @@ -294,7 +301,7 @@ flatpak install --user flathub org.gnome.Sdk//50 flatpak install --user flathub org.freedesktop.Sdk.Extension.rust-stable//25.08 ``` -The Flatpak build now produces a warning-free bundle (~47 MB compressed). No external runtime dependencies are needed — everything is sandboxed. +The Flatpak bundle includes the system tray and desktop menu entry. Find **"Viet+"** in your app launcher to start it, or search for it to uninstall. Warning-free build — no `#![allow()]` needed. See `packaging/flatpak/FLATPAK_BUILD.md` for detailed build instructions. diff --git a/daemon/src/app_state.rs b/daemon/src/app_state.rs index 36b18b1..6ea1a92 100644 --- a/daemon/src/app_state.rs +++ b/daemon/src/app_state.rs @@ -3,6 +3,114 @@ use std::collections::HashMap; use std::fs; use std::process::Command; +/// Query _NET_ACTIVE_WINDOW directly via X11 client library (dlopen). +/// Works inside the Flatpak sandbox where xdotool/xprop are unavailable +/// but libX11.so.6 is present in the GNOME runtime. No external process +/// or subclassing needed — open display, query property, return hex ID. +fn get_active_window_x11_dlopen() -> Option { + unsafe { + let lib = libc::dlopen( + b"libX11.so.6\0".as_ptr() as *const libc::c_char, + libc::RTLD_LAZY, + ); + if lib.is_null() { + return None; + } + + type FnOpenDisplay = + unsafe extern "C" fn(*const libc::c_char) -> *mut libc::c_void; + type FnDefaultRoot = + unsafe extern "C" fn(*mut libc::c_void) -> u64; + type FnInternAtom = unsafe extern "C" fn( + *mut libc::c_void, *const libc::c_char, libc::c_int, + ) -> u64; + type FnGetProperty = unsafe extern "C" fn( + *mut libc::c_void, u64, u64, u64, u64, u64, libc::c_int, + *mut u64, *mut libc::c_int, *mut u64, *mut u64, + *mut *mut u8, + ) -> libc::c_int; + type FnFree = unsafe extern "C" fn(*mut libc::c_void) -> libc::c_int; + type FnCloseDisplay = + unsafe extern "C" fn(*mut libc::c_void) -> libc::c_int; + + macro_rules! dlsym_fn { + ($lib:expr, $name:literal) => { + std::mem::transmute::<*mut libc::c_void, _>(libc::dlsym( + $lib, + concat!($name, "\0").as_ptr() as *const libc::c_char, + )) + }; + } + + let xopen: FnOpenDisplay = dlsym_fn!(lib, "XOpenDisplay"); + let xroot: FnDefaultRoot = dlsym_fn!(lib, "XDefaultRootWindow"); + let xatom: FnInternAtom = dlsym_fn!(lib, "XInternAtom"); + let xgetprop: FnGetProperty = dlsym_fn!(lib, "XGetProperty"); + let xfree: FnFree = dlsym_fn!(lib, "XFree"); + let xclosedpy: FnCloseDisplay = dlsym_fn!(lib, "XCloseDisplay"); + + let dpy = xopen(std::ptr::null()); + if dpy.is_null() { + libc::dlclose(lib); + return None; + } + + let root = xroot(dpy); + let net_active = xatom( + dpy, + b"_NET_ACTIVE_WINDOW\0".as_ptr() as *const libc::c_char, + 0, + ); + + // XA_WINDOW = 33 (the standard X11 atom for Window type) + let xa_window: u64 = 33; + let mut actual_type: u64 = 0; + let mut actual_format: libc::c_int = 0; + let mut nitems: u64 = 0; + let mut bytes_after: u64 = 0; + let mut data: *mut u8 = std::ptr::null_mut(); + + let status = xgetprop( + dpy, + root, + net_active, + xa_window, + 0, // offset + 1, // length + 0, // delete + &mut actual_type, + &mut actual_format, + &mut nitems, + &mut bytes_after, + &mut data, + ); + + let result = if status != 0 + && !data.is_null() + && nitems > 0 + && actual_format == 32 + { + // Format=32 elements are returned as unsigned long arrays + let id = *(data as *const u64); + if id != 0 { + Some(format!("0x{:x}", id)) + } else { + None + } + } else { + None + }; + + if !data.is_null() { + xfree(data as *mut libc::c_void); + } + xclosedpy(dpy); + libc::dlclose(lib); + + result + } +} + /// Get the active window's X11 ID (unique per window — even within the same /// application). Returns a unique window-identifier string. pub fn get_active_window_id() -> Option { @@ -36,6 +144,11 @@ pub fn get_active_window_id() -> Option { } } + // Final fallback: direct X11 client library query (works in Flatpak sandbox) + if let Some(id) = get_active_window_x11_dlopen() { + return Some(id); + } + None } diff --git a/packaging/flatpak/build-flatpak.sh b/packaging/flatpak/build-flatpak.sh index 5363407..94e6efa 100644 --- a/packaging/flatpak/build-flatpak.sh +++ b/packaging/flatpak/build-flatpak.sh @@ -24,10 +24,10 @@ BUILD='export PATH=/usr/lib/sdk/rust-stable/bin:$PATH export CARGO_HOME=/app/cargo cd /app/src/vietc' -# Build daemon + CLI + uinputd +# Build daemon + CLI + uinputd + tray echo "" -echo "=== Compiling daemon, CLI, uinputd... ===" -flatpak build --share=network build-dir sh -c "$BUILD && cargo build --release -p vietc-daemon -p vietc-cli -p vietc-uinputd" +echo "=== Compiling daemon, CLI, uinputd, tray... ===" +flatpak build --share=network build-dir sh -c "$BUILD && cargo build --release -p vietc-daemon -p vietc-cli -p vietc-uinputd && cargo build --release --manifest-path ui/Cargo.toml" # Install files echo "" @@ -37,6 +37,7 @@ set -e install -Dm755 /app/src/vietc/target/release/vietc /app/bin/vietc-daemon install -Dm755 /app/src/vietc/target/release/vietc-cli /app/bin/vietc-cli install -Dm755 /app/src/vietc/target/release/vietc-uinputd /app/bin/vietc-uinputd +install -Dm755 /app/src/vietc/ui/target/release/vietc-tray /app/bin/vietc-tray install -Dm644 /app/src/vietc/packaging/icons/vietc.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.svg install -Dm644 /app/src/vietc/packaging/icons/vietc-vn.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.vietc-vn.svg @@ -47,7 +48,7 @@ install -Dm644 /app/src/vietc/packaging/icons/vietc-vn.svg /app/share/icons/hico [Desktop Entry] Name=Viet+ Comment=Vietnamese Input Method -Exec=/app/bin/vietc-daemon +Exec=/app/bin/vietc-tray Icon=io.github.vietc.VietPlus Terminal=false Type=Application @@ -85,7 +86,7 @@ flatpak build-finish build-dir \ --talk-name=org.freedesktop.Notifications \ --talk-name=org.a11y.Bus \ --talk-name=org.freedesktop.portal.Clipboard \ - --command=vietc-daemon + --command=vietc-tray # Export echo "" @@ -104,3 +105,4 @@ echo "Size: $(du -h "$SCRIPT_DIR/VietPlus-${VERSION}.flatpak" | cut -f1)" echo "" echo "Install: flatpak install --user --bundle VietPlus-${VERSION}.flatpak" echo "Run: flatpak run io.github.vietc.VietPlus" +echo "Search: 'Viet+' in app menu" diff --git a/ui/src/main.rs b/ui/src/main.rs index 1ad68aa..fd72976 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -12,20 +12,31 @@ fn exe_dir() -> PathBuf { } fn find_sibling_binary(name: &str) -> String { - let sibling = exe_dir().join(name); + let dir = exe_dir(); + // Try exact name (e.g. "vietc" outside Flatpak) + let sibling = dir.join(name); if sibling.exists() { return sibling.to_string_lossy().into_owned(); } + // Try name-daemon (e.g. "vietc-daemon" inside Flatpak) + let daemon = dir.join(format!("{}-daemon", name)); + if daemon.exists() { + return daemon.to_string_lossy().into_owned(); + } name.to_string() } fn is_daemon_running() -> bool { - std::process::Command::new("pgrep") - .arg("-x") - .arg("vietc") - .status() - .map(|s| s.success()) - .unwrap_or(false) + // Check both "vietc" (outside Flatpak) and "vietc-daemon" (inside Flatpak) + let check = |name: &str| -> bool { + std::process::Command::new("pgrep") + .arg("-x") + .arg(name) + .status() + .map(|s| s.success()) + .unwrap_or(false) + }; + check("vietc") || check("vietc-daemon") } fn needs_root() -> bool {