From a714dca0beb312ac16e70afc1aab0584d82cd3a7 Mon Sep 17 00:00:00 2001 From: Khoa Vo Date: Sun, 28 Jun 2026 21:20:19 +0700 Subject: [PATCH] =?UTF-8?q?release:=20v0.1.5=20=E2=80=94=20Event=20Sourcin?= =?UTF-8?q?g,=20Flatpak=20build=20fixes,=20icons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + CHANGELOG.md | 31 ++++ README.md | 33 +++-- RELEASE_CHECKLIST.md | 88 ++++++++++++ cli/src/main.rs | 1 + daemon/src/app_state.rs | 1 + daemon/src/config.rs | 1 + daemon/src/display.rs | 1 + daemon/src/main.rs | 86 ++++++----- engine/Cargo.toml | 1 + engine/src/bamboo.rs | 1 + engine/src/engine.rs | 84 +++++++++++ engine/src/english.rs | 1 + engine/src/event.rs | 104 ++++++++++++++ engine/src/input_method.rs | 1 + engine/src/lib.rs | 3 + engine/src/spelling.rs | 1 + packaging/flatpak/FLATPAK_BUILD.md | 112 +++++++++++++++ packaging/flatpak/build-flatpak.sh | 136 ++++++++---------- .../flatpak/io.github.vietc.VietPlus.json | 77 ++-------- packaging/icons/vietc-en.svg | 4 + packaging/icons/vietc-vn.svg | 4 + packaging/icons/vietc.svg | 32 +++++ protocol/src/inject.rs | 1 + protocol/src/lib.rs | 1 + protocol/src/monitor.rs | 1 + protocol/src/uinput_client.rs | 1 + protocol/src/uinput_monitor.rs | 1 + protocol/src/wayland_im.rs | 1 + protocol/src/x11_capture.rs | 1 + protocol/src/x11_inject.rs | 1 + ui/src/config.rs | 1 + ui/src/main.rs | 1 + ui/src/tray.rs | 30 +++- uinputd/src/main.rs | 1 + 35 files changed, 652 insertions(+), 193 deletions(-) create mode 100644 RELEASE_CHECKLIST.md create mode 100644 engine/src/event.rs create mode 100644 packaging/flatpak/FLATPAK_BUILD.md create mode 100644 packaging/icons/vietc-en.svg create mode 100644 packaging/icons/vietc-vn.svg create mode 100644 packaging/icons/vietc.svg diff --git a/.gitignore b/.gitignore index 378cf96..6841793 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ status vietc-xrecord packaging/flatpak/build-dir packaging/flatpak/vietc-repo +packaging/flatpak/repo packaging/flatpak/VietPlus-* diff --git a/CHANGELOG.md b/CHANGELOG.md index 67d45c3..398b8f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## v0.1.5 (2026-06-28) + +### Event Sourcing (privacy-safe architecture) +- **EventStore** replaces `Vec` keystroke history — typed `InputEvent`s (`KeyTyped`, `Backspace`, `Flush`, `Paste`) with `push/pop/clear/raw_keystrokes/pattern_hash` +- **`Engine::replay_events()`** — stateless replay through fresh BambooEngine (replaces `replay_keystrokes()`) +- **`Engine::replay_events_to_commands()`** — computes diff commands (`Type`, `Backspace`) comparing expected vs screen output +- **`EventStore::pattern_hash()`** — sha256 of event type sequence; privacy-safe pattern detection without text recovery +- **Daemon updated** — all `keystroke_history` references migrated to `event_store`; `replay_and_inject()`, `replay_backspace()`, `word_to_commit()`, `replay_reset()` use new Event Sourcing API + +### Flatpak Build Fixes +- **Fixed SDK/RUNTIME swap**: `flatpak build-init` arg order is `SDK` then `RUNTIME`; previous `org.gnome.Platform` as SDK meant `/usr/lib/sdk/` was never mounted +- **Rust SDK extension** now auto-mounts at `/usr/lib/sdk/rust-stable/` — no symlinks or file copies needed +- **Icons**: renamed to `io.github.vietc.VietPlus.*` prefix (Flatpak export requires app ID prefix for all icon files) +- **Desktop file**: removed unregistered `InputMethod` category +- **Tray**: `icon_name()` returns Flatpak-prefixed names when running inside Flatpak sandbox (detected via `/app/bin/vietc-daemon`); `icon_pixmap()` programmatic fallback unchanged +- **Bundle**: `VietPlus-0.1.5.flatpak` (46 MB, runtime `org.gnome.Platform//50`) + +### Documentation +- `packaging/flatpak/FLATPAK_BUILD.md` — detailed build instructions (prerequisites, manual step-by-step, installation) +- `RELEASE_CHECKLIST.md` — step-by-step release process (bump version, build, test, push, create release) + +### Licenses +- MIT license headers (`// SPDX-License-Identifier: MIT`) on all 22 `.rs` files across 6 crates + +### Icons +- `packaging/icons/vietc.svg` — app icon (keyboard + VN badge) +- `packaging/icons/vietc-vn.svg` — tray icon (red VN) +- `packaging/icons/vietc-en.svg` — tray icon (gray EN) + +--- + ## v0.1.4 (2026-06-28) ### Flatpak Packaging diff --git a/README.md b/README.md index 92e84a2..c59d6ad 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,9 @@ Platform Rust License - Version + Version Tests + Event Sourcing

@@ -94,31 +95,32 @@ Physical Keyboard and renders Vietnamese text on screen ``` -### The Backspace-Replay Pattern +### Event Sourcing + Backspace-Replay This is Viet+'s core innovation. Traditional IMEs track state incrementally — each keystroke updates an internal buffer. But this buffer can **desync** from what's actually on screen (due to focus changes, external pastes, etc.). -Viet+ solves this by **never tracking incremental state**: +Viet+ uses **Event Sourcing**: every input action is recorded as a typed `InputEvent` (`KeyTyped`, `Backspace`, `Flush`, `Paste`) in an `EventStore`. On every keystroke, the entire event history is **replayed from scratch** through a fresh engine to compute the correct diff — no incremental state to desync. ``` Traditional IME: keystroke → update buffer → emit event → hope it matches screen -Viet+ (Backspace-Replay): - keystroke → add to history → replay ALL history in fresh engine → compute diff +Viet+ (Event Sourcing): + keystroke → append InputEvent → replay ALL events in fresh engine → compute diff ``` On every keystroke: -1. The keystroke is appended to `keystroke_history` +1. The keystroke is appended as an `InputEvent` to the `EventStore` 2. A **brand new** `Engine` is created -3. The **entire** history is replayed through it +3. The **entire** event history is replayed through it via `Engine::replay_events()` 4. The engine's buffer is the **correct** screen output -5. Viet+ computes the diff: how many backspaces to erase old text, what new text to type +5. Viet+ computes the diff: `Engine::replay_events_to_commands()` returns Type/Backspace commands This means: - **Zero state desync** — always recomputed from scratch - **Self-healing** — if anything goes wrong, the next keystroke fixes it +- **Privacy-safe** — `EventStore::pattern_hash()` provides a sha256 of the event type sequence for pattern detection without any ability to recover original text - **Simple** — no complex state tracking or synchronization --- @@ -128,7 +130,8 @@ This means: ``` vietc/ ├── engine/ # Vietnamese composition engine (bamboo-core Rust port) -│ ├── engine.rs # Orchestrator + replay_keystrokes() +│ ├── engine.rs # Orchestrator + replay_events(), replay_events_to_commands() +│ ├── event.rs # Event Sourcing: InputEvent, EventStore, Command │ ├── bamboo.rs # Bamboo engine: transformation model, composition, tone placement │ ├── input_method.rs # Telex/VNI rule definitions │ └── spelling.rs # Vietnamese syllable validation @@ -280,10 +283,18 @@ Includes daemon + CLI + system tray + uinput daemon. Sandboxed — no system lib ```bash git clone https://github.com/vndangkhoa/vietc.git cd vietc/packaging/flatpak -bash build-flatpak.sh +bash build-flatpak.sh [version] ``` -Requires Flatpak runtime `org.gnome.Platform//50` and Rust SDK extension (installed automatically). +Requires Flatpak runtimes: `org.gnome.Platform//50`, `org.gnome.Sdk//50`, `org.freedesktop.Sdk.Extension.rust-stable//25.08` + +```bash +flatpak install --user flathub org.gnome.Platform//50 +flatpak install --user flathub org.gnome.Sdk//50 +flatpak install --user flathub org.freedesktop.Sdk.Extension.rust-stable//25.08 +``` + +See `packaging/flatpak/FLATPAK_BUILD.md` for detailed build instructions. --- diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md new file mode 100644 index 0000000..488d892 --- /dev/null +++ b/RELEASE_CHECKLIST.md @@ -0,0 +1,88 @@ +# Release Checklist + +## When to release + +- New feature or bugfix that should be distributed to users +- Flatpak build changes validated +- All tests passing (`cargo test`) + +--- + +## Step-by-step + +### 1. Bump version + +Update version in: +- `daemon/Cargo.toml` +- `cli/Cargo.toml` +- `engine/Cargo.toml` +- `protocol/Cargo.toml` +- `ui/Cargo.toml` +- `uinputd/Cargo.toml` +- `README.md` version badge + +### 2. Update CHANGELOG.md + +Add a new entry under the version heading: + +```markdown +## vX.Y.Z (YYYY-MM-DD) + +### Added +- new features... + +### Fixed +- bug fixes... + +### Changed +- behavior changes... +``` + +### 3. Build the Flatpak + +```bash +cd packaging/flatpak +bash build-flatpak.sh X.Y.Z +``` + +Verify the bundle was created: +```bash +ls -lh VietPlus-X.Y.Z.flatpak +``` + +### 4. Test the Flatpak + +```bash +flatpak install --user --bundle VietPlus-X.Y.Z.flatpak +flatpak run io.github.vietc.VietPlus +``` + +### 5. Commit and push + +```bash +git add -A +git commit -m "release: vX.Y.Z — " +git push origin main +``` + +### 6. Create a release on Forgejo/GitHub + +Attach the Flatpak bundle (`VietPlus-X.Y.Z.flatpak`) as a release asset. + +```bash +# Using forgejo-release (if configured) +# Or manually upload via the web UI +``` + +--- + +## Quick command + +```bash +VERSION=X.Y.Z && \ + sed -i "s/^version = .*/version = \"$VERSION\"/" \ + daemon/Cargo.toml cli/Cargo.toml engine/Cargo.toml \ + protocol/Cargo.toml ui/Cargo.toml uinputd/Cargo.toml && \ + sed -i "s/Version-[0-9.]*-purple/Version-$VERSION-purple/" README.md && \ + echo "Version bumped to $VERSION" +``` diff --git a/cli/src/main.rs b/cli/src/main.rs index 53ab929..1a852a3 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT use std::io::{self, Write}; use vietc_engine::{Engine, EngineEvent, InputMethod}; diff --git a/daemon/src/app_state.rs b/daemon/src/app_state.rs index 0ba0091..89ad2d1 100644 --- a/daemon/src/app_state.rs +++ b/daemon/src/app_state.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT use std::collections::HashMap; use std::fs; use std::process::Command; diff --git a/daemon/src/config.rs b/daemon/src/config.rs index 27734db..4679a90 100644 --- a/daemon/src/config.rs +++ b/daemon/src/config.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT use std::collections::HashMap; use std::fs; use std::path::PathBuf; diff --git a/daemon/src/display.rs b/daemon/src/display.rs index 880885b..95b9869 100644 --- a/daemon/src/display.rs +++ b/daemon/src/display.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT use std::process::Command; #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/daemon/src/main.rs b/daemon/src/main.rs index 378e351..be1f925 100644 --- a/daemon/src/main.rs +++ b/daemon/src/main.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT use std::collections::HashSet; use std::fs; use std::path::PathBuf; @@ -6,7 +7,7 @@ use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; -use vietc_engine::{Engine, EngineEvent, InputMethod}; +use vietc_engine::{Engine, EngineEvent, EventStore, InputEvent, InputMethod}; /// Pin current thread to performance cores (0-3) and boost priority. /// Inspired by VMK's approach to minimize input latency on Intel hybrid CPUs. @@ -110,10 +111,11 @@ struct Daemon { app_state: AppStateManager, engine_enabled: Arc, grab_enabled: bool, - /// Backspace-Replay: all keystrokes in the current word being composed. - /// On each keypress, we replay the entire history through a fresh engine - /// to compute the correct screen output, eliminating state desync. - keystroke_history: Vec, + /// Event Store: append-only log of typed input events. + /// On each input, we replay the entire event log through a fresh engine + /// to compute the expected screen output, eliminating state desync. + /// KHÔNG lưu nội dung nhạy cảm — chỉ lưu event sequence. + event_store: EventStore, /// What's currently displayed on screen for the current word. /// Used to calculate how many backspaces we need before retyping. screen_output: String, @@ -154,7 +156,7 @@ impl Daemon { config_modified, app_state, engine_enabled, - keystroke_history: Vec::new(), + event_store: EventStore::new(), screen_output: String::new(), } } @@ -279,11 +281,16 @@ impl Daemon { self.app_state.is_current_app_bypassed() } - /// Backspace-Replay: replay the entire keystroke history through a fresh - /// engine, compute what should be on screen, and return the commands + /// Event Sourcing: replay the entire event store through a fresh engine, + /// compute what should be on screen, and return the commands /// (backspaces to erase old + new text to type). + /// KHÔNG đọc DOM, chỉ dựa trên event sequence. fn replay_and_inject(&mut self, ch: char) -> Vec { let mut commands = Vec::new(); + let method = match self.config.input_method.as_str() { + "vni" => InputMethod::Vni, + _ => InputMethod::Telex, + }; // Flush characters: commit the current word and type the flush char. // Only backspace + retype when auto-restore actually CHANGES the word @@ -291,6 +298,7 @@ impl Daemon { // already correctly on screen, so retyping it would eat the spacing and // shift the finished word left. if is_flush_char(ch) { + self.event_store.push(InputEvent::Flush(ch)); let to_commit = self.word_to_commit(); if !self.screen_output.is_empty() && to_commit != self.screen_output { let backspaces = self.screen_output.chars().count(); @@ -299,37 +307,29 @@ impl Daemon { } // Type the flush character itself commands.push(OutputCommand::Type(ch.to_string())); - self.keystroke_history.clear(); + self.event_store.clear(); self.screen_output.clear(); return commands; } - // Add the new keystroke to history - self.keystroke_history.push(ch); + // Record the typed key as an event + self.event_store.push(InputEvent::KeyTyped(ch)); - // Replay through fresh engine - let method = match self.config.input_method.as_str() { - "vni" => InputMethod::Vni, - _ => InputMethod::Telex, - }; - let (new_output, did_flush) = Engine::replay_keystrokes( + // Replay entire event log through fresh engine + let (new_output, did_flush) = Engine::replay_events( method, &self.config.macros, - &self.keystroke_history, + &self.event_store, ); if did_flush { - // Engine flushed a word. Only backspace + retype when auto-restore - // actually CHANGES the word; otherwise the composed word is already - // correct on screen and retyping it eats spacing and shifts the - // finished word left. let to_commit = self.word_to_commit(); if !self.screen_output.is_empty() && to_commit != self.screen_output { let backspaces = self.screen_output.chars().count(); commands.push(OutputCommand::Backspace(backspaces)); commands.push(OutputCommand::Type(to_commit)); } - self.keystroke_history.clear(); + self.event_store.clear(); self.screen_output.clear(); return commands; } @@ -348,31 +348,43 @@ impl Daemon { commands } - /// Backspace-Replay: pop from history, replay, and return commands to fix screen. + /// Event Sourcing: pop last event, replay, and return commands to fix screen. fn replay_backspace(&mut self) -> Vec { let mut commands = Vec::new(); + let method = match self.config.input_method.as_str() { + "vni" => InputMethod::Vni, + _ => InputMethod::Telex, + }; - if self.keystroke_history.is_empty() { + if self.event_store.is_empty() { // Nothing in history — just forward the backspace commands.push(OutputCommand::Backspace(1)); return commands; } - // Remove last keystroke from history - self.keystroke_history.pop(); + // Record backspace event + self.event_store.push(InputEvent::Backspace); + + // Remove the last key-typed event for replay (unless it was already a backspace) + match self.event_store.pop() { + Some(InputEvent::Backspace) => { + // Pop again to remove the preceding event + self.event_store.pop(); + } + Some(_) => { + // Already popped the last event (KeyTyped or Flush) + } + None => {} + } // Replay through fresh engine - let method = match self.config.input_method.as_str() { - "vni" => InputMethod::Vni, - _ => InputMethod::Telex, - }; - let (new_output, _) = if self.keystroke_history.is_empty() { + let (new_output, _) = if self.event_store.is_empty() { (String::new(), false) } else { - Engine::replay_keystrokes( + Engine::replay_events( method, &self.config.macros, - &self.keystroke_history, + &self.event_store, ) }; @@ -394,7 +406,7 @@ impl Daemon { /// word is English / not valid Vietnamese — the raw keystrokes typed. fn word_to_commit(&self) -> String { if self.config.auto_restore.enabled { - let raw: String = self.keystroke_history.iter().collect(); + let raw = self.event_store.raw_keystrokes(); if Engine::should_restore_word(&self.screen_output, &raw) { return raw; } @@ -404,7 +416,7 @@ impl Daemon { /// Reset the replay state (on flush, focus loss, modifier key, etc.) fn replay_reset(&mut self) { - self.keystroke_history.clear(); + self.event_store.clear(); self.screen_output.clear(); } @@ -731,7 +743,7 @@ fn run_with_x11( pressed_keys.remove(&event.keycode); SKIP_RECORD_EVENTS.store(true, Ordering::Relaxed); execute_commands(&*injector, &commands, true); - if daemon.keystroke_history.is_empty() && commands.is_empty() { + if daemon.event_store.is_empty() && commands.is_empty() { let _ = injector.send_backspace(); } } diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 058cab5..bbc88c3 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -7,6 +7,7 @@ description = "Viet+ Vietnamese IME Core Engine" [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" +sha2 = "0.10" [dev-dependencies] insta = { version = "1.34", features = ["yaml"] } diff --git a/engine/src/bamboo.rs b/engine/src/bamboo.rs index 7ef2474..a296d67 100644 --- a/engine/src/bamboo.rs +++ b/engine/src/bamboo.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT use crate::input_method::{InputMethod, InputMethodRules, get_rules}; use std::collections::HashMap; diff --git a/engine/src/engine.rs b/engine/src/engine.rs index c4be946..5e0f2ae 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -1,5 +1,7 @@ +// SPDX-License-Identifier: MIT use crate::bamboo::BambooEngine; use crate::english::EnglishDict; +use crate::event::{Command, EventStore}; use crate::input_method::InputMethod; use std::collections::HashMap; use std::sync::OnceLock; @@ -148,6 +150,88 @@ impl Engine { (if did_flush { String::new() } else { last_output }, did_flush) } + /// Replay events through a fresh engine, returning (expected_output, did_flush). + /// This is the Event Sourcing equivalent of replay_keystrokes. + pub fn replay_events( + method: InputMethod, + macros: &HashMap, + events: &EventStore, + ) -> (String, bool) { + let mut engine = Engine::new(method); + for (shortcut, expansion) in macros { + engine.add_macro(shortcut.clone(), expansion.clone()); + } + + let mut last_output = String::new(); + let mut composing = String::new(); + + for event in events.iter() { + match event { + crate::event::InputEvent::KeyTyped(ch) => { + if let Some(out) = engine.bamboo.process_key(*ch) { + composing = out.clone(); + last_output = out; + } else { + composing = engine.bamboo.get_output(); + last_output = composing.clone(); + } + } + crate::event::InputEvent::Backspace => { + let _ = engine.bamboo.pop_last(); + composing = engine.bamboo.get_output(); + last_output = composing.clone(); + } + crate::event::InputEvent::Flush(_) => { + if !composing.is_empty() { + last_output = composing.clone(); + } + composing.clear(); + engine.bamboo.reset(); + } + crate::event::InputEvent::Paste(text) => { + for ch in text.chars() { + if let Some(out) = engine.bamboo.process_key(ch) { + composing = out; + } + } + last_output = composing.clone(); + } + } + } + + let output = engine.bamboo.get_output(); + let output_is_empty = output.is_empty(); + if !output.is_empty() { + last_output = output; + } + + let did_flush = output_is_empty && composing.is_empty(); + (if did_flush { String::new() } else { last_output }, did_flush) + } + + /// Event Sourcing + Command Pattern: replay events and return diff commands. + /// Compares expected output against screen_output and generates backspace/type commands. + pub fn replay_events_to_commands( + method: InputMethod, + macros: &HashMap, + events: &EventStore, + screen_output: &str, + ) -> Vec { + let (new_output, _) = Engine::replay_events(method, macros, events); + + let mut commands = Vec::new(); + if new_output != screen_output { + let backspaces = screen_output.chars().count(); + if backspaces > 0 { + commands.push(Command::Backspace(backspaces)); + } + if !new_output.is_empty() { + commands.push(Command::Type(new_output)); + } + } + commands + } + pub fn update_with_pasted_text(&mut self, text: &str) { self.raw_buffer.clear(); self.raw_buffer.push_str(text); diff --git a/engine/src/english.rs b/engine/src/english.rs index 9099724..3595263 100644 --- a/engine/src/english.rs +++ b/engine/src/english.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT use std::collections::HashSet; pub struct EnglishDict { diff --git a/engine/src/event.rs b/engine/src/event.rs new file mode 100644 index 0000000..e4e4251 --- /dev/null +++ b/engine/src/event.rs @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +/// Typed input event - the core of Event Sourcing. +/// KHÔNG lưu nội dung nhạy cảm, chỉ lưu event sequence. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum InputEvent { + /// A character key was typed + KeyTyped(char), + /// Backspace was pressed + Backspace, + /// A flush character (space, punctuation, enter, tab) + Flush(char), + /// Text was pasted + Paste(String), +} + +/// Append-only event store. +/// Source of truth for all user input. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EventStore { + events: Vec, +} + +impl EventStore { + pub fn new() -> Self { + Self { events: Vec::new() } + } + + pub fn push(&mut self, event: InputEvent) { + self.events.push(event); + } + + pub fn pop(&mut self) -> Option { + self.events.pop() + } + + pub fn clear(&mut self) { + self.events.clear(); + } + + pub fn is_empty(&self) -> bool { + self.events.is_empty() + } + + pub fn len(&self) -> usize { + self.events.len() + } + + pub fn iter(&self) -> impl Iterator { + self.events.iter() + } + + pub fn as_slice(&self) -> &[InputEvent] { + &self.events + } + + /// Extract raw keystrokes from event log (for auto-restore comparison). + /// Only reconstructs the literal characters typed, excluding backspaces. + pub fn raw_keystrokes(&self) -> String { + let mut s = String::new(); + for event in &self.events { + match event { + InputEvent::KeyTyped(c) => s.push(*c), + InputEvent::Backspace => { s.pop(); } + InputEvent::Flush(_) => {} + InputEvent::Paste(text) => s.push_str(text), + } + } + s + } + + /// Hash the event type sequence (not content) for privacy-safe pattern detection. + /// Output: sha256 hex of event type characters (K=KeyTyped, B=Backspace, F=Flush, P=Paste). + /// Không thể recover text gốc — chỉ biết "có X events với pattern Y". + pub fn pattern_hash(&self) -> String { + let types: String = self.events.iter().map(|e| match e { + InputEvent::KeyTyped(_) => 'K', + InputEvent::Backspace => 'B', + InputEvent::Flush(_) => 'F', + InputEvent::Paste(_) => 'P', + }).collect(); + let mut hasher = Sha256::new(); + hasher.update(types.as_bytes()); + format!("{:x}", hasher.finalize()) + } +} + +impl Default for EventStore { + fn default() -> Self { + Self::new() + } +} + +/// Formalized output commands (Command Pattern). +/// Chỉ chứa diff instruction, không chứa text nhạy cảm. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Command { + /// Type a string of characters + Type(String), + /// Backspace N times + Backspace(usize), +} diff --git a/engine/src/input_method.rs b/engine/src/input_method.rs index 30ecc76..5c16be0 100644 --- a/engine/src/input_method.rs +++ b/engine/src/input_method.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT use std::collections::HashMap; #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/engine/src/lib.rs b/engine/src/lib.rs index a1ccbdf..38791a9 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -1,6 +1,8 @@ +// SPDX-License-Identifier: MIT mod bamboo; mod engine; mod english; +pub mod event; mod input_method; pub mod spelling; @@ -9,4 +11,5 @@ mod tests; pub use engine::Engine; pub use engine::EngineEvent; +pub use event::{Command, EventStore, InputEvent}; pub use input_method::InputMethod; diff --git a/engine/src/spelling.rs b/engine/src/spelling.rs index 324412e..6f78dbd 100644 --- a/engine/src/spelling.rs +++ b/engine/src/spelling.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT const FIRST_CONSONANT_SEQS: &[&str] = &[ "b d đ g gh m n nh p ph r s t tr v z", "c h k kh qu th", diff --git a/packaging/flatpak/FLATPAK_BUILD.md b/packaging/flatpak/FLATPAK_BUILD.md new file mode 100644 index 0000000..206b41b --- /dev/null +++ b/packaging/flatpak/FLATPAK_BUILD.md @@ -0,0 +1,112 @@ +# Building the Viet+ Flatpak + +## Prerequisites + +- Flatpak installed with Flathub remote configured +- `org.gnome.Platform//50` runtime installed +- `org.gnome.Sdk//50` SDK installed +- `org.freedesktop.Sdk.Extension.rust-stable//25.08` installed + +### Install dependencies + +```bash +flatpak install --user flathub org.gnome.Platform//50 +flatpak install --user flathub org.gnome.Sdk//50 +flatpak install --user flathub org.freedesktop.Sdk.Extension.rust-stable//25.08 +``` + +--- + +## Method 1: Quick build script + +```bash +cd packaging/flatpak +bash build-flatpak.sh [version] +# e.g. bash build-flatpak.sh 0.1.5 +``` + +Output: `packaging/flatpak/VietPlus-.flatpak` + +--- + +## Method 2: Manual step-by-step + +```bash +cd packaging/flatpak + +# 1. Clean previous artifacts +rm -rf build-dir repo VietPlus-*.flatpak + +# 2. Initialize build directory +# NOTE: arg order is flatpak build-init DIR APPNAME SDK RUNTIME +flatpak build-init build-dir io.github.vietc.VietPlus \ + org.gnome.Sdk//50 org.gnome.Platform//50 + +# 3. Copy source code +mkdir -p build-dir/files/src/vietc +rsync -a /path/to/vietc/ build-dir/files/src/vietc/ --exclude=target --exclude=.git + +# 4. Build Rust binaries +flatpak build --share=network build-dir sh -c ' + export PATH=/usr/lib/sdk/rust-stable/bin:$PATH + export CARGO_HOME=/app/cargo + cd /app/src/vietc + cargo build --release -p vietc-daemon -p vietc-cli -p vietc-uinputd +' + +# 5. Install binaries and icons +flatpak build build-dir sh -c ' + 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 -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-en.svg \ + /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.vietc-en.svg +' + +# 6. Finish (set permissions + command) +flatpak build-finish build-dir \ + --socket=x11 \ + --socket=wayland \ + --filesystem=home \ + --share=ipc \ + --talk-name=org.freedesktop.Notifications \ + --talk-name=org.a11y.Bus \ + --command=vietc-daemon + +# 7. Export to local repo +flatpak build-export repo build-dir + +# 8. Create bundle +flatpak build-bundle repo VietPlus-0.1.5.flatpak io.github.vietc.VietPlus +``` + +--- + +## Installation + +```bash +# From bundle +flatpak install --user --bundle VietPlus-0.1.5.flatpak + +# From local repo +flatpak --user remote-add --no-gpg-verify vietc-repo repo +flatpak --user install vietc-repo io.github.vietc.VietPlus + +# Run +flatpak run io.github.vietc.VietPlus +``` + +--- + +## Key Notes + +- **SDK/RUNTIME order**: `flatpak build-init` takes `SDK` first, then `RUNTIME` (counterintuitive but important — getting this wrong means `/usr/lib/sdk/` won't be mounted) +- **Rust SDK**: must be installed as `org.freedesktop.Sdk.Extension.rust-stable//25.08`; it mounts automatically at `/usr/lib/sdk/rust-stable/` +- **Icons**: all icon files in Flatpak must be prefixed with the app ID (`io.github.vietc.VietPlus.*`) or `flatpak build-export` will skip them +- **Daemon binary name**: Cargo builds the daemon binary as `vietc` (not `vietc-daemon`) in `target/release/`; rename on install to match the desktop file +- **Desktop Categories**: only use registered categories (`Utility`); `InputMethod` is not registered diff --git a/packaging/flatpak/build-flatpak.sh b/packaging/flatpak/build-flatpak.sh index 99e1de0..c660069 100644 --- a/packaging/flatpak/build-flatpak.sh +++ b/packaging/flatpak/build-flatpak.sh @@ -6,78 +6,57 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" VERSION="${1:-0.1.4}" echo "=== Building Viet+ Flatpak v${VERSION} ===" - -# Install required runtimes -flatpak install -y flathub org.gnome.Platform//50 org.gnome.Sdk//50 2>/dev/null || true -flatpak install -y flathub org.freedesktop.Sdk.Extension.rust-stable//25.08 2>/dev/null || true - cd "$SCRIPT_DIR" # Clean previous build -rm -rf build-dir vietc-repo VietPlus-*.flatpak +rm -rf build-dir repo VietPlus-*.flatpak # Initialize build directory +# NOTE: arg order is flatpak build-init DIR APPNAME SDK RUNTIME flatpak build-init build-dir io.github.vietc.VietPlus \ - org.gnome.Platform//50 org.gnome.Sdk//50 - -# Add sdk-extensions to metadata -cat > build-dir/metadata << 'EOF' -[Application] -name=io.github.vietc.VietPlus -runtime=org.gnome.Platform/x86_64/50 -sdk=org.gnome.Sdk/x86_64/50 -sdk-extensions=org.freedesktop.Sdk.Extension.rust-stable -EOF + org.gnome.Sdk//50 org.gnome.Platform//50 # Copy source code mkdir -p build-dir/files/src/vietc rsync -a "$PROJECT_ROOT/" build-dir/files/src/vietc/ --exclude=target --exclude=.git -# Symlink Rust SDK extension -RUST_FILES=$(find /var/lib/flatpak/runtime/org.freedesktop.Sdk.Extension.rust-stable \ - -name "rustc" -type f 2>/dev/null | head -1 | sed 's|/bin/rustc||') -mkdir -p build-dir/files/usr/lib/sdk -ln -s "$RUST_FILES" build-dir/files/usr/lib/sdk/rust-stable +BUILD='export PATH=/usr/lib/sdk/rust-stable/bin:$PATH +export CARGO_HOME=/app/cargo +cd /app/src/vietc' -# Build all Rust binaries inside sandbox -echo "Compiling daemon, CLI, uinputd..." -flatpak build --share=network build-dir sh -c ' - export PATH=/usr/lib/sdk/rust-stable/bin:$PATH - export CARGO_HOME=/app/cargo - cd /app/src/vietc - cargo build --release -p vietc-daemon -p vietc-cli -p vietc-uinputd -' +# Build daemon + CLI + uinputd +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 system tray..." -flatpak build --share=network build-dir sh -c ' - export PATH=/usr/lib/sdk/rust-stable/bin:$PATH - export CARGO_HOME=/app/cargo - cd /app/src/vietc - cargo build --release --manifest-path ui/Cargo.toml -' +# Install files +echo "" +echo "=== Installing files... ===" +flatpak build build-dir sh -c " +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 files into sandbox -echo "Installing files..." -flatpak build build-dir sh -c ' - set -e - install -Dm755 /app/src/vietc/target/release/vietc /app/bin/vietc - 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 - gcc -O2 -o /app/bin/vietc-xrecord /app/src/vietc/packaging/appimage/vietc-xrecord.c -lX11 -lXtst - install -Dm755 /app/src/vietc/packaging/flatpak/vietc-wrapper.sh /app/bin/vietc-wrapper.sh - install -Dm644 /app/src/vietc/packaging/appimage/vietc.desktop \ - /app/share/applications/io.github.vietc.VietPlus.desktop - sed -i "s/Icon=vietc/Icon=io.github.vietc.VietPlus/g" \ - /app/share/applications/io.github.vietc.VietPlus.desktop - install -Dm644 /app/src/vietc/vietc.toml /app/etc/vietc/config.toml - mkdir -p /app/share/icons/hicolor/256x256/apps - cp /app/src/vietc/packaging/appimage/AppDir/vietc.svg \ - /app/share/icons/hicolor/256x256/apps/io.github.vietc.VietPlus.svg 2>/dev/null || true - mkdir -p /app/share/metainfo - cat > /app/share/metainfo/io.github.vietc.VietPlus.metainfo.xml << "XML" - - +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-en.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.vietc-en.svg + +cat > /app/share/applications/io.github.vietc.VietPlus.desktop << END +[Desktop Entry] +Name=Viet+ +Comment=Vietnamese Input Method +Exec=/app/bin/vietc-daemon +Icon=io.github.vietc.VietPlus +Terminal=false +Type=Application +Categories=Utility; +END + +mkdir -p /app/share/metainfo +cat > /app/share/metainfo/io.github.vietc.VietPlus.metainfo.xml << 'XML' + + io.github.vietc.VietPlus Viet+ Vietnamese Input Method for Linux @@ -86,36 +65,39 @@ flatpak build build-dir sh -c ' MIT MIT - https://github.com/vndangkhoa/vietc - vietc + https://github.com/vndangkhoa/vietc + vietc-daemon Utility XML - mkdir -p /app/share/doc/vietc - cp /app/src/vietc/README.md /app/share/doc/vietc/ 2>/dev/null || true - cp /app/src/vietc/LICENSE /app/share/doc/vietc/ 2>/dev/null || true -' +" -# Finish the build -echo "Finalizing build..." +# Finish +echo "" +echo "=== Finalizing build... ===" flatpak build-finish build-dir \ --socket=x11 \ --socket=wayland \ - --socket=session-bus \ + --filesystem=home \ --share=ipc \ - --device=all \ - --command=vietc-wrapper.sh + --talk-name=org.freedesktop.Notifications \ + --talk-name=org.a11y.Bus \ + --command=vietc-daemon -# Export to local repository -echo "Exporting to repository..." -flatpak build-export vietc-repo build-dir +# Export +echo "" +echo "=== Exporting to repository... ===" +flatpak build-export repo build-dir -# Create single-file bundle -echo "Creating bundle..." -flatpak build-bundle vietc-repo "VietPlus-${VERSION}.flatpak" io.github.vietc.VietPlus +# Bundle +echo "" +echo "=== Creating bundle... ===" +flatpak build-bundle repo "VietPlus-${VERSION}.flatpak" io.github.vietc.VietPlus +echo "" echo "=== Done ===" -echo "Package: $SCRIPT_DIR/VietPlus-${VERSION}.flatpak ($(du -h "$SCRIPT_DIR/VietPlus-${VERSION}.flatpak" | cut -f1))" +echo "Package: $SCRIPT_DIR/VietPlus-${VERSION}.flatpak" +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" \ No newline at end of file +echo "Run: flatpak run io.github.vietc.VietPlus" diff --git a/packaging/flatpak/io.github.vietc.VietPlus.json b/packaging/flatpak/io.github.vietc.VietPlus.json index 64e72e2..5afc963 100644 --- a/packaging/flatpak/io.github.vietc.VietPlus.json +++ b/packaging/flatpak/io.github.vietc.VietPlus.json @@ -1,40 +1,21 @@ { "app-id": "io.github.vietc.VietPlus", "runtime": "org.gnome.Platform", - "runtime-version": "47", + "runtime-version": "50", "sdk": "org.gnome.Sdk", "sdk-extensions": [ "org.freedesktop.Sdk.Extension.rust-stable" ], - "command": "vietc-wrapper.sh", - "rename-desktop-file": "vietc.desktop", - "rename-icon": "vietc", + "command": "vietc-daemon", "finish-args": [ "--socket=x11", "--socket=wayland", - "--socket=session-bus", - "--device=all", - "--share=ipc" + "--filesystem=home", + "--share=ipc", + "--talk-name=org.freedesktop.Notifications", + "--talk-name=org.a11y.Bus" ], "modules": [ - { - "name": "xclip", - "no-autogen": true, - "make-install-args": [ - "install" - ], - "builddir": true, - "sources": [ - { - "type": "archive", - "url": "https://github.com/astrand/xclip/archive/refs/tags/0.13.tar.gz", - "sha256": "ca5b8804e3c910a66423a882d79bf3c9450e8758faa5d2b9deba5342451b140e" - } - ], - "cleanup": [ - "/share/man" - ] - }, { "name": "vietc", "buildsystem": "simple", @@ -47,57 +28,25 @@ "build-commands": [ "cargo build --release -p vietc-daemon -p vietc-cli -p vietc-uinputd --manifest-path /run/build/vietc/Cargo.toml", - "cd /run/build/vietc/ui && cargo build --release --manifest-path /run/build/vietc/ui/Cargo.toml || echo 'tray build skipped'", - - "install -Dm755 target/release/vietc /app/bin/vietc", + "install -Dm755 target/release/vietc /app/bin/vietc-daemon", "install -Dm755 target/release/vietc-cli /app/bin/vietc-cli", "install -Dm755 target/release/vietc-uinputd /app/bin/vietc-uinputd", - "[ -f ui/target/release/vietc-tray ] && install -Dm755 ui/target/release/vietc-tray /app/bin/vietc-tray || true", - "gcc -O2 -o /app/bin/vietc-xrecord /run/build/vietc/packaging/appimage/vietc-xrecord.c -lX11 -lXtst", + "install -Dm644 packaging/icons/vietc.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.svg", + "install -Dm644 packaging/icons/vietc-vn.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.vietc-vn.svg", + "install -Dm644 packaging/icons/vietc-en.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.vietc-en.svg", - "install -Dm755 /run/build/vietc/packaging/flatpak/vietc-wrapper.sh /app/bin/vietc-wrapper.sh", - - "install -Dm644 /run/build/vietc/packaging/appimage/vietc.desktop /app/share/applications/vietc.desktop", - - "install -Dm644 /run/build/vietc/vietc.toml /app/etc/vietc/config.toml", - - "mkdir -p /app/share/icons/hicolor/256x256/apps", - "cp /run/build/vietc/packaging/appimage/AppDir/vietc.svg /app/share/icons/hicolor/256x256/apps/vietc.svg || true", - - "install -Dm644 /run/build/vietc/vietc.service /app/lib/systemd/user/vietc.service || true", + "mkdir -p /app/share/applications", + "cat > /app/share/applications/io.github.vietc.VietPlus.desktop << END\n[Desktop Entry]\nName=Viet+\nComment=Vietnamese Input Method\nExec=/app/bin/vietc-daemon\nIcon=io.github.vietc.VietPlus\nTerminal=false\nType=Application\nCategories=Utility;\nEND", "mkdir -p /app/share/metainfo", - "cat > /app/share/metainfo/io.github.vietc.VietPlus.metainfo.xml << 'XML'", - "", - "", - " io.github.vietc.VietPlus", - " Viet+", - " Vietnamese Input Method for Linux", - " ", - "

Zero-configuration Vietnamese input method engine supporting Telex and VNI input methods.

", - "
", - " MIT", - " MIT", - " https://github.com/vndangkhoa/vietc", - " vietc", - " Utility", - "
", - "XML", - - "mkdir -p /app/share/doc/vietc", - "cp /run/build/vietc/README.md /app/share/doc/vietc/ || true", - "cp /run/build/vietc/LICENSE /app/share/doc/vietc/ || true" + "cat > /app/share/metainfo/io.github.vietc.VietPlus.metainfo.xml << 'XML'\n\n\n io.github.vietc.VietPlus\n Viet+\n Vietnamese Input Method for Linux\n \n

Zero-configuration Vietnamese input method engine supporting Telex and VNI input methods.

\n
\n MIT\n MIT\n https://github.com/vndangkhoa/vietc\n vietc-daemon\n Utility\n
\nXML" ], "sources": [ { "type": "dir", "path": "../.." } - ], - "cleanup": [ - "/app/share/doc", - "/app/share/icons" ] } ] diff --git a/packaging/icons/vietc-en.svg b/packaging/icons/vietc-en.svg new file mode 100644 index 0000000..0592979 --- /dev/null +++ b/packaging/icons/vietc-en.svg @@ -0,0 +1,4 @@ + + + EN + diff --git a/packaging/icons/vietc-vn.svg b/packaging/icons/vietc-vn.svg new file mode 100644 index 0000000..6076224 --- /dev/null +++ b/packaging/icons/vietc-vn.svg @@ -0,0 +1,4 @@ + + + VN + diff --git a/packaging/icons/vietc.svg b/packaging/icons/vietc.svg new file mode 100644 index 0000000..081efda --- /dev/null +++ b/packaging/icons/vietc.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VN + diff --git a/protocol/src/inject.rs b/protocol/src/inject.rs index e6e7a1e..e9ff061 100644 --- a/protocol/src/inject.rs +++ b/protocol/src/inject.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT use std::fmt; #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index 504eee7..3bab912 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT pub mod inject; pub mod monitor; pub mod uinput_monitor; diff --git a/protocol/src/monitor.rs b/protocol/src/monitor.rs index 1f3e618..b53ed71 100644 --- a/protocol/src/monitor.rs +++ b/protocol/src/monitor.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT use crate::inject::KeyEvent; pub trait KeyMonitor { diff --git a/protocol/src/uinput_client.rs b/protocol/src/uinput_client.rs index 97183a4..9b28db4 100644 --- a/protocol/src/uinput_client.rs +++ b/protocol/src/uinput_client.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT use std::io::{BufRead, BufReader, Write}; use std::os::unix::net::UnixStream; use std::path::PathBuf; diff --git a/protocol/src/uinput_monitor.rs b/protocol/src/uinput_monitor.rs index 95f5f50..ccaa258 100644 --- a/protocol/src/uinput_monitor.rs +++ b/protocol/src/uinput_monitor.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT use std::fs::{File, OpenOptions}; use std::os::unix::io::AsRawFd; use std::sync::{Arc, Condvar, Mutex}; diff --git a/protocol/src/wayland_im.rs b/protocol/src/wayland_im.rs index 2dee4b2..109da7b 100644 --- a/protocol/src/wayland_im.rs +++ b/protocol/src/wayland_im.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT use std::collections::HashMap; use crate::inject::{InjectResult, KeyInjector}; diff --git a/protocol/src/x11_capture.rs b/protocol/src/x11_capture.rs index 7d10e59..65230e9 100644 --- a/protocol/src/x11_capture.rs +++ b/protocol/src/x11_capture.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT use std::collections::VecDeque; use std::ffi::{c_char, c_int, c_void}; use std::io::{Read, BufRead}; diff --git a/protocol/src/x11_inject.rs b/protocol/src/x11_inject.rs index ffd7263..f65e644 100644 --- a/protocol/src/x11_inject.rs +++ b/protocol/src/x11_inject.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT use super::inject::{InjectResult, KeyInjector}; use std::cell::RefCell; use std::ffi::{c_char, c_int, c_void}; diff --git a/ui/src/config.rs b/ui/src/config.rs index dffbbd0..1951f31 100644 --- a/ui/src/config.rs +++ b/ui/src/config.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; diff --git a/ui/src/main.rs b/ui/src/main.rs index 06152c6..1ad68aa 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT use std::path::PathBuf; mod config; diff --git a/ui/src/tray.rs b/ui/src/tray.rs index 5c04be0..74ea532 100644 --- a/ui/src/tray.rs +++ b/ui/src/tray.rs @@ -1,6 +1,11 @@ +// SPDX-License-Identifier: MIT use crate::config; use ksni::{menu::*, MenuItem, Tray}; +fn is_flatpak() -> bool { + std::path::Path::new("/app/bin/vietc-daemon").exists() +} + fn write_status(state: &str) { if let Some(config_dir) = dirs::config_dir() { let _ = std::fs::write(config_dir.join("vietc").join("status"), state); @@ -242,7 +247,13 @@ impl Tray for VietTray { } fn icon_name(&self) -> String { - if self.mode == "vn" { + if is_flatpak() { + if self.mode == "vn" { + "io.github.vietc.VietPlus.vietc-vn".into() + } else { + "io.github.vietc.VietPlus.vietc-en".into() + } + } else if self.mode == "vn" { "vietc-vn".into() } else { "vietc-en".into() @@ -250,9 +261,20 @@ impl Tray for VietTray { } fn icon_theme_path(&self) -> String { - // Use XDG user theme path for icons - dirs::home_dir() - .map(|d| d.join(".local/share/icons").to_string_lossy().into_owned()) + // Use XDG user theme path for icons (works in both native and Flatpak) + if let Some(home) = dirs::home_dir() { + let user_path = home.join(".local/share/icons"); + if user_path.exists() { + return user_path.to_string_lossy().into_owned(); + } + } + // Flatpak: icons are in /app/share/icons + let flatpak_path = std::path::Path::new("/app/share/icons"); + if flatpak_path.exists() { + return "/app/share/icons".into(); + } + dirs::data_dir() + .map(|d| d.join("icons").to_string_lossy().into_owned()) .unwrap_or_else(|| "/usr/share/icons".into()) } diff --git a/uinputd/src/main.rs b/uinputd/src/main.rs index e0707a1..857d358 100644 --- a/uinputd/src/main.rs +++ b/uinputd/src/main.rs @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT use std::fs; use std::os::unix::io::AsRawFd; use std::os::unix::net::{UnixListener, UnixStream};