release: v0.1.5 — Event Sourcing, Flatpak build fixes, icons
Some checks are pending
Build & Release / Build & test (push) Waiting to run
Build & Release / Build packages (push) Blocked by required conditions

This commit is contained in:
Khoa Vo 2026-06-28 21:20:19 +07:00
parent 769d84aa80
commit a714dca0be
35 changed files with 652 additions and 193 deletions

1
.gitignore vendored
View file

@ -15,4 +15,5 @@ status
vietc-xrecord vietc-xrecord
packaging/flatpak/build-dir packaging/flatpak/build-dir
packaging/flatpak/vietc-repo packaging/flatpak/vietc-repo
packaging/flatpak/repo
packaging/flatpak/VietPlus-* packaging/flatpak/VietPlus-*

View file

@ -1,5 +1,36 @@
# Changelog # Changelog
## v0.1.5 (2026-06-28)
### Event Sourcing (privacy-safe architecture)
- **EventStore** replaces `Vec<char>` 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) ## v0.1.4 (2026-06-28)
### Flatpak Packaging ### Flatpak Packaging

View file

@ -2,8 +2,9 @@
<img src="https://img.shields.io/badge/Platform-Linux-blue?style=for-the-badge" alt="Platform"> <img src="https://img.shields.io/badge/Platform-Linux-blue?style=for-the-badge" alt="Platform">
<img src="https://img.shields.io/badge/Language-Rust-orange?style=for-the-badge" alt="Rust"> <img src="https://img.shields.io/badge/Language-Rust-orange?style=for-the-badge" alt="Rust">
<img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge" alt="License"> <img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge" alt="License">
<img src="https://img.shields.io/badge/Version-0.1.4-purple?style=for-the-badge" alt="Version"> <img src="https://img.shields.io/badge/Version-0.1.5-purple?style=for-the-badge" alt="Version">
<img src="https://img.shields.io/badge/Tests-106_passing-brightgreen?style=for-the-badge" alt="Tests"> <img src="https://img.shields.io/badge/Tests-106_passing-brightgreen?style=for-the-badge" alt="Tests">
<img src="https://img.shields.io/badge/Event_Sourcing-✓-blueviolet?style=for-the-badge" alt="Event Sourcing">
</p> </p>
<h1 align="center"> <h1 align="center">
@ -94,31 +95,32 @@ Physical Keyboard
and renders Vietnamese text on screen 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.). 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: Traditional IME:
keystroke → update buffer → emit event → hope it matches screen keystroke → update buffer → emit event → hope it matches screen
Viet+ (Backspace-Replay): Viet+ (Event Sourcing):
keystroke → add to history → replay ALL history in fresh engine → compute diff keystroke → append InputEvent → replay ALL events in fresh engine → compute diff
``` ```
On every keystroke: 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 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 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: This means:
- **Zero state desync** — always recomputed from scratch - **Zero state desync** — always recomputed from scratch
- **Self-healing** — if anything goes wrong, the next keystroke fixes it - **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 - **Simple** — no complex state tracking or synchronization
--- ---
@ -128,7 +130,8 @@ This means:
``` ```
vietc/ vietc/
├── engine/ # Vietnamese composition engine (bamboo-core Rust port) ├── 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 │ ├── bamboo.rs # Bamboo engine: transformation model, composition, tone placement
│ ├── input_method.rs # Telex/VNI rule definitions │ ├── input_method.rs # Telex/VNI rule definitions
│ └── spelling.rs # Vietnamese syllable validation │ └── spelling.rs # Vietnamese syllable validation
@ -280,10 +283,18 @@ Includes daemon + CLI + system tray + uinput daemon. Sandboxed — no system lib
```bash ```bash
git clone https://github.com/vndangkhoa/vietc.git git clone https://github.com/vndangkhoa/vietc.git
cd vietc/packaging/flatpak 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.
--- ---

88
RELEASE_CHECKLIST.md Normal file
View file

@ -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 — <summary>"
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"
```

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
use std::io::{self, Write}; use std::io::{self, Write};
use vietc_engine::{Engine, EngineEvent, InputMethod}; use vietc_engine::{Engine, EngineEvent, InputMethod};

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
use std::collections::HashMap; use std::collections::HashMap;
use std::fs; use std::fs;
use std::process::Command; use std::process::Command;

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
use std::collections::HashMap; use std::collections::HashMap;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
use std::process::Command; use std::process::Command;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
use std::collections::HashSet; use std::collections::HashSet;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
@ -6,7 +7,7 @@ use std::sync::{Arc, Mutex};
use std::thread; use std::thread;
use std::time::Duration; 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. /// Pin current thread to performance cores (0-3) and boost priority.
/// Inspired by VMK's approach to minimize input latency on Intel hybrid CPUs. /// Inspired by VMK's approach to minimize input latency on Intel hybrid CPUs.
@ -110,10 +111,11 @@ struct Daemon {
app_state: AppStateManager, app_state: AppStateManager,
engine_enabled: Arc<AtomicBool>, engine_enabled: Arc<AtomicBool>,
grab_enabled: bool, grab_enabled: bool,
/// Backspace-Replay: all keystrokes in the current word being composed. /// Event Store: append-only log of typed input events.
/// On each keypress, we replay the entire history through a fresh engine /// On each input, we replay the entire event log through a fresh engine
/// to compute the correct screen output, eliminating state desync. /// to compute the expected screen output, eliminating state desync.
keystroke_history: Vec<char>, /// 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. /// What's currently displayed on screen for the current word.
/// Used to calculate how many backspaces we need before retyping. /// Used to calculate how many backspaces we need before retyping.
screen_output: String, screen_output: String,
@ -154,7 +156,7 @@ impl Daemon {
config_modified, config_modified,
app_state, app_state,
engine_enabled, engine_enabled,
keystroke_history: Vec::new(), event_store: EventStore::new(),
screen_output: String::new(), screen_output: String::new(),
} }
} }
@ -279,11 +281,16 @@ impl Daemon {
self.app_state.is_current_app_bypassed() self.app_state.is_current_app_bypassed()
} }
/// Backspace-Replay: replay the entire keystroke history through a fresh /// Event Sourcing: replay the entire event store through a fresh engine,
/// engine, compute what should be on screen, and return the commands /// compute what should be on screen, and return the commands
/// (backspaces to erase old + new text to type). /// (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<OutputCommand> { fn replay_and_inject(&mut self, ch: char) -> Vec<OutputCommand> {
let mut commands = Vec::new(); 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. // Flush characters: commit the current word and type the flush char.
// Only backspace + retype when auto-restore actually CHANGES the word // 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 // already correctly on screen, so retyping it would eat the spacing and
// shift the finished word left. // shift the finished word left.
if is_flush_char(ch) { if is_flush_char(ch) {
self.event_store.push(InputEvent::Flush(ch));
let to_commit = self.word_to_commit(); let to_commit = self.word_to_commit();
if !self.screen_output.is_empty() && to_commit != self.screen_output { if !self.screen_output.is_empty() && to_commit != self.screen_output {
let backspaces = self.screen_output.chars().count(); let backspaces = self.screen_output.chars().count();
@ -299,37 +307,29 @@ impl Daemon {
} }
// Type the flush character itself // Type the flush character itself
commands.push(OutputCommand::Type(ch.to_string())); commands.push(OutputCommand::Type(ch.to_string()));
self.keystroke_history.clear(); self.event_store.clear();
self.screen_output.clear(); self.screen_output.clear();
return commands; return commands;
} }
// Add the new keystroke to history // Record the typed key as an event
self.keystroke_history.push(ch); self.event_store.push(InputEvent::KeyTyped(ch));
// Replay through fresh engine // Replay entire event log through fresh engine
let method = match self.config.input_method.as_str() { let (new_output, did_flush) = Engine::replay_events(
"vni" => InputMethod::Vni,
_ => InputMethod::Telex,
};
let (new_output, did_flush) = Engine::replay_keystrokes(
method, method,
&self.config.macros, &self.config.macros,
&self.keystroke_history, &self.event_store,
); );
if did_flush { 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(); let to_commit = self.word_to_commit();
if !self.screen_output.is_empty() && to_commit != self.screen_output { if !self.screen_output.is_empty() && to_commit != self.screen_output {
let backspaces = self.screen_output.chars().count(); let backspaces = self.screen_output.chars().count();
commands.push(OutputCommand::Backspace(backspaces)); commands.push(OutputCommand::Backspace(backspaces));
commands.push(OutputCommand::Type(to_commit)); commands.push(OutputCommand::Type(to_commit));
} }
self.keystroke_history.clear(); self.event_store.clear();
self.screen_output.clear(); self.screen_output.clear();
return commands; return commands;
} }
@ -348,31 +348,43 @@ impl Daemon {
commands 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<OutputCommand> { fn replay_backspace(&mut self) -> Vec<OutputCommand> {
let mut commands = Vec::new(); 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 // Nothing in history — just forward the backspace
commands.push(OutputCommand::Backspace(1)); commands.push(OutputCommand::Backspace(1));
return commands; return commands;
} }
// Remove last keystroke from history // Record backspace event
self.keystroke_history.pop(); 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 // Replay through fresh engine
let method = match self.config.input_method.as_str() { let (new_output, _) = if self.event_store.is_empty() {
"vni" => InputMethod::Vni,
_ => InputMethod::Telex,
};
let (new_output, _) = if self.keystroke_history.is_empty() {
(String::new(), false) (String::new(), false)
} else { } else {
Engine::replay_keystrokes( Engine::replay_events(
method, method,
&self.config.macros, &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. /// word is English / not valid Vietnamese — the raw keystrokes typed.
fn word_to_commit(&self) -> String { fn word_to_commit(&self) -> String {
if self.config.auto_restore.enabled { 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) { if Engine::should_restore_word(&self.screen_output, &raw) {
return raw; return raw;
} }
@ -404,7 +416,7 @@ impl Daemon {
/// Reset the replay state (on flush, focus loss, modifier key, etc.) /// Reset the replay state (on flush, focus loss, modifier key, etc.)
fn replay_reset(&mut self) { fn replay_reset(&mut self) {
self.keystroke_history.clear(); self.event_store.clear();
self.screen_output.clear(); self.screen_output.clear();
} }
@ -731,7 +743,7 @@ fn run_with_x11(
pressed_keys.remove(&event.keycode); pressed_keys.remove(&event.keycode);
SKIP_RECORD_EVENTS.store(true, Ordering::Relaxed); SKIP_RECORD_EVENTS.store(true, Ordering::Relaxed);
execute_commands(&*injector, &commands, true); 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(); let _ = injector.send_backspace();
} }
} }

View file

@ -7,6 +7,7 @@ description = "Viet+ Vietnamese IME Core Engine"
[dependencies] [dependencies]
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
sha2 = "0.10"
[dev-dependencies] [dev-dependencies]
insta = { version = "1.34", features = ["yaml"] } insta = { version = "1.34", features = ["yaml"] }

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
use crate::input_method::{InputMethod, InputMethodRules, get_rules}; use crate::input_method::{InputMethod, InputMethodRules, get_rules};
use std::collections::HashMap; use std::collections::HashMap;

View file

@ -1,5 +1,7 @@
// SPDX-License-Identifier: MIT
use crate::bamboo::BambooEngine; use crate::bamboo::BambooEngine;
use crate::english::EnglishDict; use crate::english::EnglishDict;
use crate::event::{Command, EventStore};
use crate::input_method::InputMethod; use crate::input_method::InputMethod;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::OnceLock; use std::sync::OnceLock;
@ -148,6 +150,88 @@ impl Engine {
(if did_flush { String::new() } else { last_output }, did_flush) (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<String, String>,
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<String, String>,
events: &EventStore,
screen_output: &str,
) -> Vec<Command> {
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) { pub fn update_with_pasted_text(&mut self, text: &str) {
self.raw_buffer.clear(); self.raw_buffer.clear();
self.raw_buffer.push_str(text); self.raw_buffer.push_str(text);

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
use std::collections::HashSet; use std::collections::HashSet;
pub struct EnglishDict { pub struct EnglishDict {

104
engine/src/event.rs Normal file
View file

@ -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<InputEvent>,
}
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<InputEvent> {
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<Item = &InputEvent> {
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),
}

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]

View file

@ -1,6 +1,8 @@
// SPDX-License-Identifier: MIT
mod bamboo; mod bamboo;
mod engine; mod engine;
mod english; mod english;
pub mod event;
mod input_method; mod input_method;
pub mod spelling; pub mod spelling;
@ -9,4 +11,5 @@ mod tests;
pub use engine::Engine; pub use engine::Engine;
pub use engine::EngineEvent; pub use engine::EngineEvent;
pub use event::{Command, EventStore, InputEvent};
pub use input_method::InputMethod; pub use input_method::InputMethod;

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
const FIRST_CONSONANT_SEQS: &[&str] = &[ const FIRST_CONSONANT_SEQS: &[&str] = &[
"b d đ g gh m n nh p ph r s t tr v z", "b d đ g gh m n nh p ph r s t tr v z",
"c h k kh qu th", "c h k kh qu th",

View file

@ -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-<version>.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

View file

@ -6,78 +6,57 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
VERSION="${1:-0.1.4}" VERSION="${1:-0.1.4}"
echo "=== Building Viet+ Flatpak v${VERSION} ===" 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" cd "$SCRIPT_DIR"
# Clean previous build # Clean previous build
rm -rf build-dir vietc-repo VietPlus-*.flatpak rm -rf build-dir repo VietPlus-*.flatpak
# Initialize build directory # Initialize build directory
# NOTE: arg order is flatpak build-init DIR APPNAME SDK RUNTIME
flatpak build-init build-dir io.github.vietc.VietPlus \ flatpak build-init build-dir io.github.vietc.VietPlus \
org.gnome.Platform//50 org.gnome.Sdk//50 org.gnome.Sdk//50 org.gnome.Platform//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
# Copy source code # Copy source code
mkdir -p build-dir/files/src/vietc mkdir -p build-dir/files/src/vietc
rsync -a "$PROJECT_ROOT/" build-dir/files/src/vietc/ --exclude=target --exclude=.git rsync -a "$PROJECT_ROOT/" build-dir/files/src/vietc/ --exclude=target --exclude=.git
# Symlink Rust SDK extension BUILD='export PATH=/usr/lib/sdk/rust-stable/bin:$PATH
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 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 export CARGO_HOME=/app/cargo
cd /app/src/vietc cd /app/src/vietc'
cargo build --release -p vietc-daemon -p vietc-cli -p vietc-uinputd
'
echo "Compiling system tray..." # Build daemon + CLI + uinputd
flatpak build --share=network build-dir sh -c ' echo ""
export PATH=/usr/lib/sdk/rust-stable/bin:$PATH echo "=== Compiling daemon, CLI, uinputd... ==="
export CARGO_HOME=/app/cargo flatpak build --share=network build-dir sh -c "$BUILD && cargo build --release -p vietc-daemon -p vietc-cli -p vietc-uinputd"
cd /app/src/vietc
cargo build --release --manifest-path ui/Cargo.toml
'
# Install files into sandbox # Install files
echo "Installing files..." echo ""
flatpak build build-dir sh -c ' echo "=== Installing files... ==="
flatpak build build-dir sh -c "
set -e set -e
install -Dm755 /app/src/vietc/target/release/vietc /app/bin/vietc 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
gcc -O2 -o /app/bin/vietc-xrecord /app/src/vietc/packaging/appimage/vietc-xrecord.c -lX11 -lXtst install -Dm644 /app/src/vietc/packaging/icons/vietc.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.svg
install -Dm755 /app/src/vietc/packaging/flatpak/vietc-wrapper.sh /app/bin/vietc-wrapper.sh 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/appimage/vietc.desktop \ install -Dm644 /app/src/vietc/packaging/icons/vietc-en.svg /app/share/icons/hicolor/scalable/apps/io.github.vietc.VietPlus.vietc-en.svg
/app/share/applications/io.github.vietc.VietPlus.desktop
sed -i "s/Icon=vietc/Icon=io.github.vietc.VietPlus/g" \ cat > /app/share/applications/io.github.vietc.VietPlus.desktop << END
/app/share/applications/io.github.vietc.VietPlus.desktop [Desktop Entry]
install -Dm644 /app/src/vietc/vietc.toml /app/etc/vietc/config.toml Name=Viet+
mkdir -p /app/share/icons/hicolor/256x256/apps Comment=Vietnamese Input Method
cp /app/src/vietc/packaging/appimage/AppDir/vietc.svg \ Exec=/app/bin/vietc-daemon
/app/share/icons/hicolor/256x256/apps/io.github.vietc.VietPlus.svg 2>/dev/null || true Icon=io.github.vietc.VietPlus
Terminal=false
Type=Application
Categories=Utility;
END
mkdir -p /app/share/metainfo mkdir -p /app/share/metainfo
cat > /app/share/metainfo/io.github.vietc.VietPlus.metainfo.xml << "XML" cat > /app/share/metainfo/io.github.vietc.VietPlus.metainfo.xml << 'XML'
<?xml version="1.0" encoding="utf-8"?> <?xml version='1.0' encoding='utf-8'?>
<component type="desktop-application"> <component type='desktop-application'>
<id>io.github.vietc.VietPlus</id> <id>io.github.vietc.VietPlus</id>
<name>Viet+</name> <name>Viet+</name>
<summary>Vietnamese Input Method for Linux</summary> <summary>Vietnamese Input Method for Linux</summary>
@ -86,36 +65,39 @@ flatpak build build-dir sh -c '
</description> </description>
<metadata_license>MIT</metadata_license> <metadata_license>MIT</metadata_license>
<project_license>MIT</project_license> <project_license>MIT</project_license>
<url type="homepage">https://github.com/vndangkhoa/vietc</url> <url type='homepage'>https://github.com/vndangkhoa/vietc</url>
<provides><binary>vietc</binary></provides> <provides><binary>vietc-daemon</binary></provides>
<categories><category>Utility</category></categories> <categories><category>Utility</category></categories>
</component> </component>
XML 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 # Finish
echo "Finalizing build..." echo ""
echo "=== Finalizing build... ==="
flatpak build-finish build-dir \ flatpak build-finish build-dir \
--socket=x11 \ --socket=x11 \
--socket=wayland \ --socket=wayland \
--socket=session-bus \ --filesystem=home \
--share=ipc \ --share=ipc \
--device=all \ --talk-name=org.freedesktop.Notifications \
--command=vietc-wrapper.sh --talk-name=org.a11y.Bus \
--command=vietc-daemon
# Export to local repository # Export
echo "Exporting to repository..." echo ""
flatpak build-export vietc-repo build-dir echo "=== Exporting to repository... ==="
flatpak build-export repo build-dir
# Create single-file bundle # Bundle
echo "Creating bundle..." echo ""
flatpak build-bundle vietc-repo "VietPlus-${VERSION}.flatpak" io.github.vietc.VietPlus echo "=== Creating bundle... ==="
flatpak build-bundle repo "VietPlus-${VERSION}.flatpak" io.github.vietc.VietPlus
echo ""
echo "=== Done ===" 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 ""
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"

View file

@ -1,40 +1,21 @@
{ {
"app-id": "io.github.vietc.VietPlus", "app-id": "io.github.vietc.VietPlus",
"runtime": "org.gnome.Platform", "runtime": "org.gnome.Platform",
"runtime-version": "47", "runtime-version": "50",
"sdk": "org.gnome.Sdk", "sdk": "org.gnome.Sdk",
"sdk-extensions": [ "sdk-extensions": [
"org.freedesktop.Sdk.Extension.rust-stable" "org.freedesktop.Sdk.Extension.rust-stable"
], ],
"command": "vietc-wrapper.sh", "command": "vietc-daemon",
"rename-desktop-file": "vietc.desktop",
"rename-icon": "vietc",
"finish-args": [ "finish-args": [
"--socket=x11", "--socket=x11",
"--socket=wayland", "--socket=wayland",
"--socket=session-bus", "--filesystem=home",
"--device=all", "--share=ipc",
"--share=ipc" "--talk-name=org.freedesktop.Notifications",
"--talk-name=org.a11y.Bus"
], ],
"modules": [ "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", "name": "vietc",
"buildsystem": "simple", "buildsystem": "simple",
@ -47,57 +28,25 @@
"build-commands": [ "build-commands": [
"cargo build --release -p vietc-daemon -p vietc-cli -p vietc-uinputd --manifest-path /run/build/vietc/Cargo.toml", "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-daemon",
"install -Dm755 target/release/vietc /app/bin/vietc",
"install -Dm755 target/release/vietc-cli /app/bin/vietc-cli", "install -Dm755 target/release/vietc-cli /app/bin/vietc-cli",
"install -Dm755 target/release/vietc-uinputd /app/bin/vietc-uinputd", "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", "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",
"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/metainfo", "mkdir -p /app/share/metainfo",
"cat > /app/share/metainfo/io.github.vietc.VietPlus.metainfo.xml << 'XML'", "cat > /app/share/metainfo/io.github.vietc.VietPlus.metainfo.xml << 'XML'\n<?xml version='1.0' encoding='utf-8'?>\n<component type='desktop-application'>\n <id>io.github.vietc.VietPlus</id>\n <name>Viet+</name>\n <summary>Vietnamese Input Method for Linux</summary>\n <description>\n <p>Zero-configuration Vietnamese input method engine supporting Telex and VNI input methods.</p>\n </description>\n <metadata_license>MIT</metadata_license>\n <project_license>MIT</project_license>\n <url type='homepage'>https://github.com/vndangkhoa/vietc</url>\n <provides><binary>vietc-daemon</binary></provides>\n <categories><category>Utility</category></categories>\n</component>\nXML"
"<?xml version='1.0' encoding='utf-8'?>",
"<component type='desktop-application'>",
" <id>io.github.vietc.VietPlus</id>",
" <name>Viet+</name>",
" <summary>Vietnamese Input Method for Linux</summary>",
" <description>",
" <p>Zero-configuration Vietnamese input method engine supporting Telex and VNI input methods.</p>",
" </description>",
" <metadata_license>MIT</metadata_license>",
" <project_license>MIT</project_license>",
" <url type='homepage'>https://github.com/vndangkhoa/vietc</url>",
" <provides><binary>vietc</binary></provides>",
" <categories><category>Utility</category></categories>",
"</component>",
"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"
], ],
"sources": [ "sources": [
{ {
"type": "dir", "type": "dir",
"path": "../.." "path": "../.."
} }
],
"cleanup": [
"/app/share/doc",
"/app/share/icons"
] ]
} }
] ]

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<rect x="8" y="8" width="112" height="112" rx="24" fill="#4b5563"/>
<text x="64" y="96" text-anchor="middle" fill="#ffffff" font-size="48" font-weight="bold" font-family="system-ui, sans-serif">EN</text>
</svg>

After

Width:  |  Height:  |  Size: 278 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<rect x="8" y="8" width="112" height="112" rx="24" fill="#e02424"/>
<text x="64" y="96" text-anchor="middle" fill="#ffffff" font-size="48" font-weight="bold" font-family="system-ui, sans-serif">VN</text>
</svg>

After

Width:  |  Height:  |  Size: 278 B

32
packaging/icons/vietc.svg Normal file
View file

@ -0,0 +1,32 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
<defs>
<linearGradient id="kb-bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#3a3a3a"/>
<stop offset="100%" stop-color="#1a1a1a"/>
</linearGradient>
<linearGradient id="vn-badge" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#e74c3c"/>
<stop offset="100%" stop-color="#c0392b"/>
</linearGradient>
</defs>
<rect x="16" y="56" width="224" height="156" rx="18" fill="url(#kb-bg)" stroke="#555" stroke-width="3"/>
<rect x="32" y="72" width="192" height="124" rx="10" fill="#2a2a2a"/>
<rect x="44" y="84" width="26" height="22" rx="4" fill="#e0e0e0"/>
<rect x="76" y="84" width="26" height="22" rx="4" fill="#e0e0e0"/>
<rect x="108" y="84" width="26" height="22" rx="4" fill="#e0e0e0"/>
<rect x="140" y="84" width="26" height="22" rx="4" fill="#e0e0e0"/>
<rect x="172" y="84" width="26" height="22" rx="4" fill="#e0e0e0"/>
<rect x="50" y="112" width="26" height="22" rx="4" fill="#e0e0e0"/>
<rect x="82" y="112" width="26" height="22" rx="4" fill="#e0e0e0"/>
<rect x="114" y="112" width="26" height="22" rx="4" fill="#e0e0e0"/>
<rect x="146" y="112" width="26" height="22" rx="4" fill="#e0e0e0"/>
<rect x="178" y="112" width="26" height="22" rx="4" fill="#e0e0e0"/>
<rect x="56" y="140" width="26" height="22" rx="4" fill="#e0e0e0"/>
<rect x="88" y="140" width="26" height="22" rx="4" fill="#e0e0e0"/>
<rect x="120" y="140" width="26" height="22" rx="4" fill="#e0e0e0"/>
<rect x="152" y="140" width="26" height="22" rx="4" fill="#e0e0e0"/>
<rect x="184" y="140" width="30" height="22" rx="4" fill="#e0e0e0"/>
<rect x="68" y="168" width="120" height="16" rx="4" fill="#e0e0e0"/>
<circle cx="224" cy="44" r="30" fill="url(#vn-badge)"/>
<text x="224" y="52" text-anchor="middle" fill="#fff" font-size="20" font-weight="bold" font-family="sans-serif">VN</text>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
use std::fmt; use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
pub mod inject; pub mod inject;
pub mod monitor; pub mod monitor;
pub mod uinput_monitor; pub mod uinput_monitor;

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
use crate::inject::KeyEvent; use crate::inject::KeyEvent;
pub trait KeyMonitor { pub trait KeyMonitor {

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
use std::io::{BufRead, BufReader, Write}; use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use std::path::PathBuf; use std::path::PathBuf;

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::os::unix::io::AsRawFd; use std::os::unix::io::AsRawFd;
use std::sync::{Arc, Condvar, Mutex}; use std::sync::{Arc, Condvar, Mutex};

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
use std::collections::HashMap; use std::collections::HashMap;
use crate::inject::{InjectResult, KeyInjector}; use crate::inject::{InjectResult, KeyInjector};

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
use std::collections::VecDeque; use std::collections::VecDeque;
use std::ffi::{c_char, c_int, c_void}; use std::ffi::{c_char, c_int, c_void};
use std::io::{Read, BufRead}; use std::io::{Read, BufRead};

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
use super::inject::{InjectResult, KeyInjector}; use super::inject::{InjectResult, KeyInjector};
use std::cell::RefCell; use std::cell::RefCell;
use std::ffi::{c_char, c_int, c_void}; use std::ffi::{c_char, c_int, c_void};

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
use std::path::PathBuf; use std::path::PathBuf;
mod config; mod config;

View file

@ -1,6 +1,11 @@
// SPDX-License-Identifier: MIT
use crate::config; use crate::config;
use ksni::{menu::*, MenuItem, Tray}; use ksni::{menu::*, MenuItem, Tray};
fn is_flatpak() -> bool {
std::path::Path::new("/app/bin/vietc-daemon").exists()
}
fn write_status(state: &str) { fn write_status(state: &str) {
if let Some(config_dir) = dirs::config_dir() { if let Some(config_dir) = dirs::config_dir() {
let _ = std::fs::write(config_dir.join("vietc").join("status"), state); let _ = std::fs::write(config_dir.join("vietc").join("status"), state);
@ -242,7 +247,13 @@ impl Tray for VietTray {
} }
fn icon_name(&self) -> String { fn icon_name(&self) -> String {
if is_flatpak() {
if self.mode == "vn" { 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() "vietc-vn".into()
} else { } else {
"vietc-en".into() "vietc-en".into()
@ -250,9 +261,20 @@ impl Tray for VietTray {
} }
fn icon_theme_path(&self) -> String { fn icon_theme_path(&self) -> String {
// Use XDG user theme path for icons // Use XDG user theme path for icons (works in both native and Flatpak)
dirs::home_dir() if let Some(home) = dirs::home_dir() {
.map(|d| d.join(".local/share/icons").to_string_lossy().into_owned()) 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()) .unwrap_or_else(|| "/usr/share/icons".into())
} }

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: MIT
use std::fs; use std::fs;
use std::os::unix::io::AsRawFd; use std::os::unix::io::AsRawFd;
use std::os::unix::net::{UnixListener, UnixStream}; use std::os::unix::net::{UnixListener, UnixStream};