feat: Flatpak tray, X11 dlopen window query, desktop menu entry
Some checks are pending
Build & Release / Build & test (push) Waiting to run
Build & Release / Build packages (push) Blocked by required conditions

- 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
This commit is contained in:
Khoa Vo 2026-06-29 14:32:30 +07:00
parent 1198d65543
commit 98ce9def79
5 changed files with 156 additions and 16 deletions

View file

@ -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`. - **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). - **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. - **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.
--- ---

View file

@ -270,10 +270,17 @@ Flexible typing: type the full syllable, then add marks/tone keys at the end. Ex
### Flatpak (recommended) ### Flatpak (recommended)
System tray icon + daemon. Find **"Viet+"** in your app menu to launch, or run from terminal.
```bash ```bash
# Download from the release page, then: # Install
flatpak install --user --bundle VietPlus-x86_64.flatpak flatpak install --user --bundle VietPlus-x86_64.flatpak
# Launch via app menu, or:
flatpak run io.github.vietc.VietPlus 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. 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 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. See `packaging/flatpak/FLATPAK_BUILD.md` for detailed build instructions.

View file

@ -3,6 +3,114 @@ use std::collections::HashMap;
use std::fs; use std::fs;
use std::process::Command; 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<String> {
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 /// Get the active window's X11 ID (unique per window — even within the same
/// application). Returns a unique window-identifier string. /// application). Returns a unique window-identifier string.
pub fn get_active_window_id() -> Option<String> { pub fn get_active_window_id() -> Option<String> {
@ -36,6 +144,11 @@ pub fn get_active_window_id() -> Option<String> {
} }
} }
// Final fallback: direct X11 client library query (works in Flatpak sandbox)
if let Some(id) = get_active_window_x11_dlopen() {
return Some(id);
}
None None
} }

View file

@ -24,10 +24,10 @@ BUILD='export PATH=/usr/lib/sdk/rust-stable/bin:$PATH
export CARGO_HOME=/app/cargo export CARGO_HOME=/app/cargo
cd /app/src/vietc' cd /app/src/vietc'
# Build daemon + CLI + uinputd # Build daemon + CLI + uinputd + tray
echo "" echo ""
echo "=== Compiling daemon, CLI, 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" 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 # Install files
echo "" 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 /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-cli /app/bin/vietc-cli
install -Dm755 /app/src/vietc/target/release/vietc-uinputd /app/bin/vietc-uinputd 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.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 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] [Desktop Entry]
Name=Viet+ Name=Viet+
Comment=Vietnamese Input Method Comment=Vietnamese Input Method
Exec=/app/bin/vietc-daemon Exec=/app/bin/vietc-tray
Icon=io.github.vietc.VietPlus Icon=io.github.vietc.VietPlus
Terminal=false Terminal=false
Type=Application Type=Application
@ -85,7 +86,7 @@ flatpak build-finish build-dir \
--talk-name=org.freedesktop.Notifications \ --talk-name=org.freedesktop.Notifications \
--talk-name=org.a11y.Bus \ --talk-name=org.a11y.Bus \
--talk-name=org.freedesktop.portal.Clipboard \ --talk-name=org.freedesktop.portal.Clipboard \
--command=vietc-daemon --command=vietc-tray
# Export # Export
echo "" echo ""
@ -104,3 +105,4 @@ echo "Size: $(du -h "$SCRIPT_DIR/VietPlus-${VERSION}.flatpak" | cut -f1)"
echo "" echo ""
echo "Install: flatpak install --user --bundle VietPlus-${VERSION}.flatpak" echo "Install: flatpak install --user --bundle VietPlus-${VERSION}.flatpak"
echo "Run: flatpak run io.github.vietc.VietPlus" echo "Run: flatpak run io.github.vietc.VietPlus"
echo "Search: 'Viet+' in app menu"

View file

@ -12,20 +12,31 @@ fn exe_dir() -> PathBuf {
} }
fn find_sibling_binary(name: &str) -> String { 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() { if sibling.exists() {
return sibling.to_string_lossy().into_owned(); 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() name.to_string()
} }
fn is_daemon_running() -> bool { fn is_daemon_running() -> bool {
std::process::Command::new("pgrep") // Check both "vietc" (outside Flatpak) and "vietc-daemon" (inside Flatpak)
.arg("-x") let check = |name: &str| -> bool {
.arg("vietc") std::process::Command::new("pgrep")
.status() .arg("-x")
.map(|s| s.success()) .arg(name)
.unwrap_or(false) .status()
.map(|s| s.success())
.unwrap_or(false)
};
check("vietc") || check("vietc-daemon")
} }
fn needs_root() -> bool { fn needs_root() -> bool {