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 @@
-
+
+
@@ -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 @@
+
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 @@
+
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 @@
+
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};